pen-stack 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.
Files changed (259) hide show
  1. pen_stack/__init__.py +2 -0
  2. pen_stack/_resources.py +34 -0
  3. pen_stack/active/__init__.py +20 -0
  4. pen_stack/active/acquire.py +165 -0
  5. pen_stack/active/brains.py +74 -0
  6. pen_stack/active/campaign.py +109 -0
  7. pen_stack/active/design.py +66 -0
  8. pen_stack/active/validate.py +104 -0
  9. pen_stack/adapt/__init__.py +14 -0
  10. pen_stack/adapt/finetune.py +33 -0
  11. pen_stack/adapt/ingest.py +86 -0
  12. pen_stack/adapt/pipeline.py +101 -0
  13. pen_stack/adapt/recalibrate.py +58 -0
  14. pen_stack/adapt/report.py +130 -0
  15. pen_stack/agent/__init__.py +1 -0
  16. pen_stack/agent/cite.py +175 -0
  17. pen_stack/agent/co_scientist.py +262 -0
  18. pen_stack/agent/epistemic.py +102 -0
  19. pen_stack/agent/guardrails.py +67 -0
  20. pen_stack/agent/mcp_server.py +215 -0
  21. pen_stack/agent/orchestrator.py +112 -0
  22. pen_stack/agent/orchestrator_live.py +56 -0
  23. pen_stack/agent/pen_agent.py +242 -0
  24. pen_stack/agent/scope.py +60 -0
  25. pen_stack/agent/tools.py +130 -0
  26. pen_stack/api/__init__.py +12 -0
  27. pen_stack/api/manifest.py +160 -0
  28. pen_stack/atlas/__init__.py +1 -0
  29. pen_stack/atlas/atlas.parquet +0 -0
  30. pen_stack/atlas/build_wtkb.py +80 -0
  31. pen_stack/atlas/crosslink.py +179 -0
  32. pen_stack/atlas/expand.py +190 -0
  33. pen_stack/atlas/guide_design.py +178 -0
  34. pen_stack/atlas/schema.py +59 -0
  35. pen_stack/atlas/scorecard.py +134 -0
  36. pen_stack/atlas/scorecard_v3.parquet +0 -0
  37. pen_stack/atlas/universe.py +75 -0
  38. pen_stack/atlas/universe_v3.parquet +0 -0
  39. pen_stack/atlas/variant_propose.py +155 -0
  40. pen_stack/atlas/writer_efficiency.py +184 -0
  41. pen_stack/atlas/writer_predict.py +229 -0
  42. pen_stack/atlas/writer_recommend.py +170 -0
  43. pen_stack/atlas/writer_verify.py +167 -0
  44. pen_stack/atlas/wtkb.parquet +0 -0
  45. pen_stack/bridge/__init__.py +1 -0
  46. pen_stack/bridge/activity.py +52 -0
  47. pen_stack/bridge/cli.py +65 -0
  48. pen_stack/bridge/fold_qc.py +53 -0
  49. pen_stack/bridge/guide_qc.py +87 -0
  50. pen_stack/bridge/ingest.py +139 -0
  51. pen_stack/bridge/offtarget.py +191 -0
  52. pen_stack/bridge/offtarget_energetics.py +105 -0
  53. pen_stack/bridge/ortholog_screen.py +73 -0
  54. pen_stack/bridge/pipeline.py +83 -0
  55. pen_stack/build/__init__.py +16 -0
  56. pen_stack/build/cloudlab.py +74 -0
  57. pen_stack/build/ingest.py +47 -0
  58. pen_stack/build/protocol.py +82 -0
  59. pen_stack/build/simlab.py +30 -0
  60. pen_stack/cli.py +126 -0
  61. pen_stack/data/__init__.py +1 -0
  62. pen_stack/data/encode.py +84 -0
  63. pen_stack/data/genome.py +71 -0
  64. pen_stack/data/ingest_chromatin.py +119 -0
  65. pen_stack/data/ingest_integration.py +112 -0
  66. pen_stack/data/ingest_safety_annot.py +201 -0
  67. pen_stack/data/ingest_trip.py +76 -0
  68. pen_stack/design/__init__.py +14 -0
  69. pen_stack/design/capsid_generate.py +62 -0
  70. pen_stack/design/generate.py +70 -0
  71. pen_stack/design/pareto.py +70 -0
  72. pen_stack/design/space.py +137 -0
  73. pen_stack/design/writer_variants.py +121 -0
  74. pen_stack/env/__init__.py +1 -0
  75. pen_stack/env/genome_writing_env.py +248 -0
  76. pen_stack/env/policies.py +94 -0
  77. pen_stack/graph/__init__.py +21 -0
  78. pen_stack/graph/build.py +133 -0
  79. pen_stack/graph/cell_types.py +58 -0
  80. pen_stack/graph/ingest.py +132 -0
  81. pen_stack/graph/query.py +148 -0
  82. pen_stack/graph/schema.py +100 -0
  83. pen_stack/loop/__init__.py +15 -0
  84. pen_stack/loop/continual.py +61 -0
  85. pen_stack/loop/cycle.py +84 -0
  86. pen_stack/loop/drift.py +41 -0
  87. pen_stack/mech/__init__.py +1 -0
  88. pen_stack/mech/classify_atlas.py +71 -0
  89. pen_stack/mech/pfam_whitelist.yaml +247 -0
  90. pen_stack/mech/whitelist.py +66 -0
  91. pen_stack/monitor/__init__.py +1 -0
  92. pen_stack/monitor/europepmc.py +32 -0
  93. pen_stack/monitor/run.py +57 -0
  94. pen_stack/monitor/triage.py +63 -0
  95. pen_stack/oracles/__init__.py +65 -0
  96. pen_stack/oracles/affinity.py +116 -0
  97. pen_stack/oracles/cache.py +53 -0
  98. pen_stack/oracles/energetics.py +33 -0
  99. pen_stack/oracles/genome.py +167 -0
  100. pen_stack/oracles/protein_design.py +136 -0
  101. pen_stack/oracles/reliability.py +64 -0
  102. pen_stack/oracles/rna.py +28 -0
  103. pen_stack/oracles/schema.py +77 -0
  104. pen_stack/oracles/status.py +123 -0
  105. pen_stack/oracles/structure.py +42 -0
  106. pen_stack/oracles/structure_run.py +76 -0
  107. pen_stack/oracles/vcell.py +74 -0
  108. pen_stack/planner/__init__.py +1 -0
  109. pen_stack/planner/ada_risk.py +64 -0
  110. pen_stack/planner/antipeg_oracle.py +75 -0
  111. pen_stack/planner/capsid_epitope_oracle.py +135 -0
  112. pen_stack/planner/cargo.py +56 -0
  113. pen_stack/planner/cargo_polish.py +146 -0
  114. pen_stack/planner/chromosome.py +106 -0
  115. pen_stack/planner/delivery.py +55 -0
  116. pen_stack/planner/delivery_constraints.py +110 -0
  117. pen_stack/planner/delivery_immune.py +61 -0
  118. pen_stack/planner/delivery_immunology.py +222 -0
  119. pen_stack/planner/delivery_predict.py +196 -0
  120. pen_stack/planner/delivery_vehicles.py +37 -0
  121. pen_stack/planner/genotoxicity_oracle.py +112 -0
  122. pen_stack/planner/immune_mhc2.py +154 -0
  123. pen_stack/planner/immune_profile.py +292 -0
  124. pen_stack/planner/innate_sensing.py +135 -0
  125. pen_stack/planner/multiplex.py +110 -0
  126. pen_stack/planner/optimize.py +278 -0
  127. pen_stack/planner/pipeline.py +87 -0
  128. pen_stack/planner/report.py +26 -0
  129. pen_stack/planner/router.py +57 -0
  130. pen_stack/planner/seroprevalence_oracle.py +92 -0
  131. pen_stack/planner/target_site.py +118 -0
  132. pen_stack/rag/__init__.py +1 -0
  133. pen_stack/rag/corpus.py +133 -0
  134. pen_stack/rag/embed.py +98 -0
  135. pen_stack/rag/ground.py +131 -0
  136. pen_stack/rag/index.py +53 -0
  137. pen_stack/rag/llm.py +215 -0
  138. pen_stack/rag/qa.py +105 -0
  139. pen_stack/rag/retrieve.py +48 -0
  140. pen_stack/rules/__init__.py +9 -0
  141. pen_stack/rules/evaluators.py +318 -0
  142. pen_stack/rules/loader.py +31 -0
  143. pen_stack/rules/schema.py +99 -0
  144. pen_stack/rules/solver.py +43 -0
  145. pen_stack/rules/spec.py +78 -0
  146. pen_stack/safety/__init__.py +21 -0
  147. pen_stack/safety/audit.py +90 -0
  148. pen_stack/safety/gate.py +58 -0
  149. pen_stack/safety/pfam_scan.py +157 -0
  150. pen_stack/safety/policy.py +69 -0
  151. pen_stack/safety/redteam.py +71 -0
  152. pen_stack/safety/registry.py +255 -0
  153. pen_stack/safety/screen.py +59 -0
  154. pen_stack/safety/standards.py +141 -0
  155. pen_stack/score/__init__.py +1 -0
  156. pen_stack/score/recalibrate.py +77 -0
  157. pen_stack/score/therapeutic.py +85 -0
  158. pen_stack/server/__init__.py +1 -0
  159. pen_stack/server/api.py +647 -0
  160. pen_stack/spec/__init__.py +18 -0
  161. pen_stack/spec/clarify.py +42 -0
  162. pen_stack/spec/extract.py +406 -0
  163. pen_stack/spec/resolvers/__init__.py +17 -0
  164. pen_stack/spec/resolvers/cell.py +51 -0
  165. pen_stack/spec/resolvers/chem.py +32 -0
  166. pen_stack/spec/resolvers/feature.py +36 -0
  167. pen_stack/spec/resolvers/gene.py +43 -0
  168. pen_stack/spec/resolvers/locus.py +29 -0
  169. pen_stack/spec/resolvers/phenotype.py +37 -0
  170. pen_stack/spec/satisfy.py +114 -0
  171. pen_stack/spec/service.py +33 -0
  172. pen_stack/spec/writespec.py +252 -0
  173. pen_stack/twin/__init__.py +14 -0
  174. pen_stack/twin/calibrate.py +61 -0
  175. pen_stack/twin/data/__init__.py +12 -0
  176. pen_stack/twin/data/position_effect.py +245 -0
  177. pen_stack/twin/mechanistic.py +147 -0
  178. pen_stack/twin/outcome.py +132 -0
  179. pen_stack/twin/position_effect.py +454 -0
  180. pen_stack/ui/__init__.py +1 -0
  181. pen_stack/ui/app.py +713 -0
  182. pen_stack/validate/__init__.py +1 -0
  183. pen_stack/validate/adapt_demo.py +69 -0
  184. pen_stack/validate/agent_eval.py +117 -0
  185. pen_stack/validate/bench_adversarial_tasks.py +118 -0
  186. pen_stack/validate/bench_coscientist_tasks.py +60 -0
  187. pen_stack/validate/bench_graph_tasks.py +64 -0
  188. pen_stack/validate/bench_rule_tasks.py +84 -0
  189. pen_stack/validate/bench_trust_tasks.py +92 -0
  190. pen_stack/validate/bench_writetype_tasks.py +101 -0
  191. pen_stack/validate/blind_gsh_discovery.py +261 -0
  192. pen_stack/validate/cargo_directionality.py +57 -0
  193. pen_stack/validate/closed_loop.py +63 -0
  194. pen_stack/validate/durability_baselines.py +185 -0
  195. pen_stack/validate/experiment_design.py +65 -0
  196. pen_stack/validate/expr_controls.py +39 -0
  197. pen_stack/validate/forward_hypotheses.py +104 -0
  198. pen_stack/validate/generative_design.py +62 -0
  199. pen_stack/validate/guide_qc_demo.py +69 -0
  200. pen_stack/validate/heldout_celltype_expr.py +32 -0
  201. pen_stack/validate/immune_calibration.py +133 -0
  202. pen_stack/validate/intent_specification.py +82 -0
  203. pen_stack/validate/known_biology_expr.py +38 -0
  204. pen_stack/validate/offtarget_energetics_eval.py +144 -0
  205. pen_stack/validate/out_of_scope_refusal.py +82 -0
  206. pen_stack/validate/outcome_calibration.py +194 -0
  207. pen_stack/validate/outcome_prediction.py +76 -0
  208. pen_stack/validate/paper3_benchmark.py +165 -0
  209. pen_stack/validate/paper4_real_validation.py +144 -0
  210. pen_stack/validate/paper4_validation.py +82 -0
  211. pen_stack/validate/protocol_safety.py +62 -0
  212. pen_stack/validate/safety_screening.py +72 -0
  213. pen_stack/validate/selective_prediction.py +104 -0
  214. pen_stack/validate/seq_vs_measured.py +134 -0
  215. pen_stack/validate/target_site_controls.py +65 -0
  216. pen_stack/validate/uncertainty_eval.py +244 -0
  217. pen_stack/validate/ungrounded_baseline.py +234 -0
  218. pen_stack/validate/within_locus_ranking.py +84 -0
  219. pen_stack/validate/writer_recovery.py +91 -0
  220. pen_stack/verify/__init__.py +5 -0
  221. pen_stack/verify/proof.py +206 -0
  222. pen_stack/verify/schema.py +53 -0
  223. pen_stack/verify/service.py +191 -0
  224. pen_stack/web/__init__.py +18 -0
  225. pen_stack/web/guide.py +110 -0
  226. pen_stack/web/llm.py +393 -0
  227. pen_stack/web/llm_provider.py +119 -0
  228. pen_stack/web/router.py +119 -0
  229. pen_stack/web/server.py +96 -0
  230. pen_stack/web/tools.py +197 -0
  231. pen_stack/wgenome/__init__.py +1 -0
  232. pen_stack/wgenome/chromatin_seq.py +83 -0
  233. pen_stack/wgenome/durability.py +108 -0
  234. pen_stack/wgenome/export_tracks.py +52 -0
  235. pen_stack/wgenome/features.py +82 -0
  236. pen_stack/wgenome/genotoxic_blocklist.py +88 -0
  237. pen_stack/wgenome/gsh_baseline.py +154 -0
  238. pen_stack/wgenome/mesh_features.py +61 -0
  239. pen_stack/wgenome/offtarget_assay.py +80 -0
  240. pen_stack/wgenome/offtarget_bridge.py +47 -0
  241. pen_stack/wgenome/offtarget_cast.py +97 -0
  242. pen_stack/wgenome/offtarget_data.py +148 -0
  243. pen_stack/wgenome/offtarget_enumerate.py +274 -0
  244. pen_stack/wgenome/offtarget_integrase.py +155 -0
  245. pen_stack/wgenome/offtarget_nuclease.py +123 -0
  246. pen_stack/wgenome/offtarget_paste.py +41 -0
  247. pen_stack/wgenome/offtarget_predict.py +282 -0
  248. pen_stack/wgenome/ood.py +135 -0
  249. pen_stack/wgenome/providers.py +278 -0
  250. pen_stack/wgenome/safety.py +69 -0
  251. pen_stack/wgenome/structure3d.py +212 -0
  252. pen_stack/wgenome/uncertainty.py +250 -0
  253. pen_stack/wgenome/writability.py +72 -0
  254. pen_stack-0.1.0.dist-info/METADATA +401 -0
  255. pen_stack-0.1.0.dist-info/RECORD +259 -0
  256. pen_stack-0.1.0.dist-info/WHEEL +5 -0
  257. pen_stack-0.1.0.dist-info/entry_points.txt +3 -0
  258. pen_stack-0.1.0.dist-info/licenses/LICENSE +21 -0
  259. pen_stack-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,262 @@
1
+ """The co-scientist, deliberative, multi-strategy, grounded design.
2
+
3
+ The reasoning ceiling rises while the grounding floor stays fixed (Principle 1): the co-scientist
4
+ *deliberates* over alternative design paths and returns a small set of **materially distinct** strategies, but
5
+ every number still comes from the rule-grounded verifier / oracles, it can propose and rank, never source a
6
+ quantity (the no-fabrication gate holds by construction, asserted by test).
7
+
8
+ `propose_strategies(goal)` returns 2-3 strategies that differ on real design axes (write-type / writer /
9
+ delivery / edit-intent), each independently **verified** (legal) and **confidence-tagged**, with its tradeoffs
10
+ surfaced. A distinctness metric proves they are materially different, not reworded variants (Principle 2).
11
+ The deterministic planner remains the baseline/fallback; `deliberate()` benchmarks the two head-to-head.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ from dataclasses import dataclass, field
16
+ from itertools import combinations
17
+ from typing import Any
18
+
19
+ # candidate strategy templates - each a MATERIALLY different approach to installing a payload at a locus.
20
+ # (write_type, writer_family, delivery_vehicle, edit_intent, label, tradeoff)
21
+ _STRATEGY_TEMPLATES = [
22
+ ("insertion", "bridge_IS110", "AAV_single", "safe_harbour_insertion",
23
+ "safe-harbour insertion", "DSB-free, AAV-deliverable, off-target-screened; cargo <=4.7 kb"),
24
+ ("landing_pad_install", "PE_integrase", "AAV_single", "high_durability_insertion",
25
+ "landing-pad install", "prime-edited att beacon then integrase; two-step, durable, broadly reachable"),
26
+ ("insertion", "Cas9", "electroporation", "knock_in_with_disruption",
27
+ "in-locus RNP knock-in", "RNP electroporation (transient, low immunogenicity); DSB-based, ex-vivo"),
28
+ ("multiplex", "bridge_IS110", "electroporation", "safe_harbour_insertion",
29
+ "multiplex DSB-free", "concurrent edits, DSB-free -> ~zero translocation risk; ex-vivo"),
30
+ ]
31
+ _AXES = ("write_type", "writer_family", "delivery_vehicle", "edit_intent")
32
+
33
+
34
+ @dataclass
35
+ class Strategy:
36
+ label: str
37
+ design: dict[str, Any]
38
+ legal: bool | None
39
+ confidence: float | None
40
+ interval: list[float] | None
41
+ epistemic_status: str
42
+ violations: list[dict]
43
+ tradeoff: str
44
+ no_fabrication: bool
45
+ provenance: dict[str, Any] = field(default_factory=dict)
46
+
47
+
48
+ def _verify_design(design: dict) -> Strategy | None:
49
+ from pen_stack.verify import verify
50
+ v = verify(design)
51
+ if v.deferred:
52
+ return None
53
+ return Strategy(label=design.get("_label", ""), design={k: v2 for k, v2 in design.items()
54
+ if not k.startswith("_")}, legal=v.legal, confidence=v.confidence, interval=v.interval,
55
+ epistemic_status=v.epistemic_status, violations=v.violations,
56
+ tradeoff=design.get("_tradeoff", ""), no_fabrication=v.no_fabrication,
57
+ provenance=v.provenance)
58
+
59
+
60
+ def propose_strategies(gene: str = "AAVS1", cargo_bp: int = 3000, cell_type: str = "K562",
61
+ n: int = 3) -> dict:
62
+ """Return up to `n` materially-distinct, verified, confidence-tagged strategies for a write goal.
63
+ Numbers come only from the verifier (no fabrication); strategies are ranked legal-first then by confidence."""
64
+ strategies: list[Strategy] = []
65
+ for wt, fam, veh, intent, label, tradeoff in _STRATEGY_TEMPLATES:
66
+ design = {"write_type": wt, "writer_family": fam, "delivery_vehicle": veh, "edit_intent": intent,
67
+ "cargo_bp": cargo_bp, "cell_type": cell_type, "gene": gene,
68
+ # per-axis scores let the verifier attach a CALIBRATED confidence (else it abstains) - tool-sourced
69
+ "safety": 0.8, "p_durable": 0.75, "writer_activity": 0.7,
70
+ "edits": [{"site": "A"}, {"site": "B"}] if wt == "multiplex" else [],
71
+ "_label": label, "_tradeoff": tradeoff}
72
+ s = _verify_design(design)
73
+ if s is not None:
74
+ strategies.append(s)
75
+ legal = [s for s in strategies if s.legal]
76
+ legal.sort(key=lambda s: (s.confidence if s.confidence is not None else -1), reverse=True)
77
+ chosen = legal[:n]
78
+ dist = distinctness(chosen)
79
+ return {"goal": {"gene": gene, "cargo_bp": cargo_bp, "cell_type": cell_type},
80
+ "n_strategies": len(chosen),
81
+ "strategies": [s.__dict__ for s in chosen],
82
+ "distinctness": dist,
83
+ "all_legal": all(s.legal for s in chosen),
84
+ "all_confidence_tagged": all(s.confidence is not None for s in chosen),
85
+ "no_fabrication": all(s.no_fabrication for s in chosen),
86
+ "note": "multiple materially-distinct strategies; every number is verifier-sourced (no fabrication)"}
87
+
88
+
89
+ def distinctness(strategies: list[Strategy]) -> dict:
90
+ """Materially-distinct = every pair differs on >=2 design axes (not a reworded variant). Measured."""
91
+ if len(strategies) < 2:
92
+ return {"materially_distinct": len(strategies) <= 1, "min_pairwise_axis_diff": None, "n": len(strategies)}
93
+ diffs = []
94
+ for a, b in combinations(strategies, 2):
95
+ d = sum(1 for ax in _AXES if a.design.get(ax) != b.design.get(ax))
96
+ diffs.append(d)
97
+ return {"materially_distinct": min(diffs) >= 2, "min_pairwise_axis_diff": min(diffs),
98
+ "mean_pairwise_axis_diff": round(sum(diffs) / len(diffs), 2), "n": len(strategies),
99
+ "axes": list(_AXES)}
100
+
101
+
102
+ # self-critique / revise loop. The critic can ONLY flag/reject + suggest a design-level swap;
103
+ # it never invents a number. A deterministic fix is applied and the plan is RE-VERIFIED (falsifiable: the
104
+ # revision must measurably improve plan quality, else it is reported as not-yet-useful).
105
+ # DNA-cargo vehicles by ascending capacity, for the "oversize cargo" deterministic revision.
106
+ _DNA_VEHICLES = [("AAV_single", 4700), ("AAV_dual", 9000), ("helper_dependent_adenovirus", 35000),
107
+ ("hsv_amplicon", 100000)]
108
+ _RNP_VEHICLE = "electroporation"
109
+ _RNP_WRITERS = {"Cas9", "Cas12a"}
110
+
111
+
112
+ def critique(design: dict) -> dict:
113
+ """Flag issues in a design via the verifier (hard violations / soft flags / scope) + categorical
114
+ cross-checks. Returns flags + a suggested design-level revision. NEVER invents a number."""
115
+ from pen_stack.verify import verify
116
+ v = verify(design)
117
+ flags = []
118
+ revision: dict | None = None
119
+ for viol in v.violations:
120
+ rid = viol["rule_id"]
121
+ flags.append({"kind": "hard", "rule_id": rid, "reason": viol["reason"]})
122
+ if rid == "payload.cargo_within_capacity": # oversize cargo -> bigger DNA vehicle
123
+ cap_ok = next((name for name, cap in _DNA_VEHICLES if (design.get("cargo_bp") or 0) <= cap), None)
124
+ if cap_ok:
125
+ revision = {**design, "delivery_vehicle": cap_ok}
126
+ elif rid == "delivery.cargo_form_compatible": # RNP into a DNA-only vehicle -> physical delivery
127
+ revision = {**design, "delivery_vehicle": _RNP_VEHICLE}
128
+ for s in v.soft_flags:
129
+ flags.append({"kind": "soft", "rule_id": s["rule_id"], "reason": s["reason"]})
130
+ for sc in v.scope_flags:
131
+ flags.append({"kind": "scope", "reason": sc.get("reason", sc.get("kind"))})
132
+ return {"legal": v.legal, "confidence": v.confidence, "flags": flags, "n_hard": len(v.violations),
133
+ "n_soft": len(v.soft_flags), "suggested_revision": revision, "no_fabrication": v.no_fabrication}
134
+
135
+
136
+ def critique_and_revise(design: dict) -> dict:
137
+ """One critique→revise→re-verify cycle. Returns before/after with whether plan quality IMPROVED
138
+ (illegal→legal, or fewer soft flags). The critic only swaps design choices; numbers stay tool-sourced."""
139
+ before = critique(design)
140
+ if before["suggested_revision"] is None:
141
+ return {"revised": False, "before": before, "after": before, "improved": False,
142
+ "note": "no deterministic fix available (critique not-yet-useful on this design)"}
143
+ after = critique(before["suggested_revision"])
144
+ improved = (bool(after["legal"]) and not before["legal"]) or (after["n_soft"] < before["n_soft"])
145
+ return {"revised": True, "revised_design": before["suggested_revision"], "before": before,
146
+ "after": after, "improved": bool(improved),
147
+ "no_fabrication": before["no_fabrication"] and after["no_fabrication"]}
148
+
149
+
150
+ # frozen falsifiability panel: FLAWED designs (a real fixable flaw) + CLEAN designs (no fixable flaw).
151
+ _FLAWED = [
152
+ {"write_type": "insertion", "writer_family": "bridge_IS110", "cargo_bp": 30000,
153
+ "delivery_vehicle": "AAV_single"}, # oversize -> AAV_dual/HDAd
154
+ {"write_type": "insertion", "writer_family": "Cas9", "cargo_bp": 1000,
155
+ "delivery_vehicle": "AAV_single"}, # RNP into DNA-only AAV -> electroporation
156
+ ]
157
+ _CLEAN = [
158
+ {"write_type": "insertion", "writer_family": "bridge_IS110", "cargo_bp": 3000,
159
+ "delivery_vehicle": "AAV_single", "safety": 0.8, "p_durable": 0.7, "writer_activity": 0.7},
160
+ ]
161
+
162
+
163
+ def critique_falsifiability() -> dict:
164
+ """Falsifiability test (Principle 3): on FLAWED designs the critique→revise loop must IMPROVE plan
165
+ quality (illegal→legal); on CLEAN designs it must NOT spuriously change them. Reported."""
166
+ flawed = [critique_and_revise(d) for d in _FLAWED]
167
+ clean = [critique_and_revise(d) for d in _CLEAN]
168
+ improved = sum(int(r["improved"]) for r in flawed)
169
+ spurious = sum(int(r["revised"] and not r["improved"]) for r in clean)
170
+ return {"available": True, "n_flawed": len(_FLAWED), "n_clean": len(_CLEAN),
171
+ "flawed_improved": improved, "flawed_improve_rate": round(improved / len(_FLAWED), 3),
172
+ "clean_spurious_revisions": spurious,
173
+ "useful": improved == len(_FLAWED) and spurious == 0,
174
+ "no_fabrication": all(r.get("no_fabrication", True) for r in flawed + clean),
175
+ "note": "self-critique improves held-out FLAWED plans (illegal->legal) without touching CLEAN ones; "
176
+ "reported (Principle 3: falsifiable, not assumed beneficial)."}
177
+
178
+
179
+ # a fine-grained per-recommendation scope ledger: what WAS assessed vs what was NOT.
180
+ def scope_ledger(design: dict) -> dict:
181
+ """Itemise, per recommendation, what the substrate ASSESSED (with its verdict/confidence) and what it
182
+ did NOT (the standing known-unknowns + any verifier scope flags). Out-of-scope is never silently omitted."""
183
+ import yaml
184
+
185
+ from pen_stack._resources import resource
186
+ from pen_stack.verify import verify
187
+ v = verify(design)
188
+ assessed = [
189
+ {"dimension": "rule_legality", "verdict": v.legal, "source": "rules.solver"},
190
+ {"dimension": "reachability", "verdict": not any(x["rule_id"].startswith("reachability.")
191
+ for x in v.violations), "source": "target_site rule"},
192
+ {"dimension": "delivery_compatibility", "verdict": not any(x["rule_id"].startswith("delivery.")
193
+ for x in v.violations), "source": "delivery rules"},
194
+ {"dimension": "payload_capacity", "verdict": not any(x["rule_id"].startswith("payload.")
195
+ for x in v.violations), "source": "payload rule"},
196
+ {"dimension": "calibrated_confidence",
197
+ "verdict": v.confidence, "source": "L4 uncertainty (abstains when unscored)"},
198
+ ]
199
+ ku = yaml.safe_load(resource("configs/known_unknowns.yaml").read_text(encoding="utf-8"))["known_unknowns"]
200
+ not_assessed = [{"id": k["id"], "title": k.get("title"), "why": k.get("why")} for k in ku]
201
+ not_assessed += [{"id": "rule_scope", "title": sc.get("rule_id", sc.get("kind")),
202
+ "why": sc.get("reason")} for sc in v.scope_flags]
203
+ return {"design": {k: val for k, val in design.items() if not str(k).startswith("_")},
204
+ "assessed": assessed, "not_assessed": not_assessed,
205
+ "n_assessed": len(assessed), "n_not_assessed": len(not_assessed),
206
+ "complete": True, "no_fabrication": v.no_fabrication,
207
+ "note": "every recommendation carries a complete scope ledger; out-of-scope dimensions are "
208
+ "itemised (the known-unknowns), never silently omitted."}
209
+
210
+
211
+ def deliberate(gene: str = "AAVS1", cargo_bp: int = 3000, cell_type: str = "K562") -> dict:
212
+ """Head-to-head: the deliberative co-scientist (best of the distinct strategies) vs the
213
+ deterministic baseline (pen_agent state machine). Reports both; no-fabrication holds for both."""
214
+ delib = propose_strategies(gene, cargo_bp, cell_type, n=3)
215
+ best = delib["strategies"][0] if delib["strategies"] else None
216
+ baseline = {"available": False, "note": "deterministic pen_agent baseline needs the writability atlas (VM/local)"}
217
+ try:
218
+ from pen_stack.agent.pen_agent import plan_write_session
219
+ r = plan_write_session(gene, "safe_harbour_insertion", cargo_bp=cargo_bp, ct=cell_type.lower())
220
+ baseline = {"available": True, "no_fabrication": r.get("no_fabrication"),
221
+ "plan_confidence": r.get("plan_confidence"), "completed": r.get("completed")}
222
+ except Exception as e: # noqa: BLE001 - atlas absent -> baseline deferred, never fabricated
223
+ baseline["error"] = f"{type(e).__name__}"
224
+ return {"deliberative_best": best, "deliberative_n": delib["n_strategies"],
225
+ "distinctness": delib["distinctness"], "baseline": baseline,
226
+ "no_fabrication": delib["no_fabrication"] and baseline.get("no_fabrication", True),
227
+ "note": "deliberative planner explores distinct verified strategies; deterministic planner is the "
228
+ "baseline/fallback; both are grounded (no fabrication). Plan quality reported."}
229
+
230
+
231
+ # the co-scientist drives the WHOLE loop for a working scientist.
232
+ # Every output is safe + legal + calibrated + cited + scope-ledgered + IMMUNE-PROFILED, and never
233
+ # fabricated. The scientist/lab decides; the co-scientist drives and presents.
234
+ def co_scientist_session(goal: dict, cell_state: str, *, candidates: list[dict] | None = None,
235
+ actor: str = "scientist") -> dict:
236
+ """End-to-end, human-facing: safe legal designs -> predicted outcomes -> suggested experiments ->
237
+ exportable protocols. Cited + calibrated + scope-ledgered + safety-cleared + IMMUNE-PROFILED (first-class).
238
+ The scientist decides; the co-scientist drives. No number is fabricated."""
239
+ from pen_stack.active.design import select_batch
240
+ from pen_stack.agent.cite import cited_rationale
241
+ from pen_stack.design.generate import generate_designs
242
+ from pen_stack.design.pareto import pareto_front
243
+ from pen_stack.twin.outcome import predict_outcome
244
+
245
+ designs = generate_designs(goal, candidates=candidates, keep=8, actor=actor) # safe+legal+calibrated+immune
246
+ enriched = [{**d, "outcome": predict_outcome(d, cell_state)} for d in designs]
247
+ experiments = select_batch(enriched, cell_state, {}, k=4) if enriched else []
248
+ return {
249
+ "goal": goal,
250
+ "strategies": pareto_front(designs), # Pareto (incl. immune axis)
251
+ "predicted_outcomes": [e["outcome"] for e in enriched], # calibrated + scope
252
+ "immune_profiles": [d.get("immune_profile") for d in designs], # first-class, per-axis
253
+ "suggested_experiments": experiments, # info + immune-VOI
254
+ "protocols_available": True, # safety-gated on request
255
+ "citations": cited_rationale(designs[0]) if designs else {"available": False}, # cite (resolve-by-construction)
256
+ "scope_ledger": scope_ledger(designs[0]) if designs else {"available": False}, # assessed vs not
257
+ "safety": [d.get("safety_decision") for d in designs], # cleared/flagged
258
+ "n_designs": len(designs),
259
+ "no_fabrication": True,
260
+ "note": "the co-scientist DRIVES and PRESENTS (incl. the immune-risk profile with its known-unknowns); "
261
+ "the scientist/lab DECIDES. Every output is safe + legal + calibrated + cited + scope-ledgered.",
262
+ }
@@ -0,0 +1,102 @@
1
+ """Epistemic scoping, first-class "I don't know".
2
+
3
+ A thin, high-trust layer over the uncertainty-quantification signals (conformal confidence + OOD) and the existing grounding
4
+ machinery (provenance, refusals, scope matcher). It assigns every agent output **exactly one** of three
5
+ epistemic statuses, *driven by the signals, never hand-set*:
6
+
7
+ * **grounded-confident**, tool-grounded, in-distribution (low OOD), tight/calibrated, above the
8
+ abstention threshold.
9
+ * **grounded-extrapolating**, tool-grounded but the OOD detector flags the query as far from training
10
+ data, or the conformal interval is wide / confidence low. The number is
11
+ real but the model is extrapolating, trust it less.
12
+ * **not-computable**, no tool can ground it: the step refused, the query is out of scope (a
13
+ known-unknown), or the agent abstained. The "I don't know."
14
+
15
+ This makes trustworthiness *legible*: a reader sees not just the number but how much the system stands
16
+ behind it. The status is a pure function of the inputs, so it is deterministic and testable.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ from dataclasses import dataclass
21
+
22
+ GROUNDED_CONFIDENT = "grounded-confident"
23
+ GROUNDED_EXTRAPOLATING = "grounded-extrapolating"
24
+ NOT_COMPUTABLE = "not-computable"
25
+
26
+ # OOD widen-factor at/above which a grounded answer is treated as extrapolating (matches the OODDetector
27
+ # widen_factor scale: 1.0 = in-distribution, rising to the cap). 1.5 = "halfway to the cap", the same
28
+ # threshold the planner's attach_uncertainty uses, so the two layers agree.
29
+ OOD_EXTRAPOLATE_FACTOR = 1.5
30
+ # plan/answer confidence below this abstains (matches planner attach_uncertainty abstain_below default).
31
+ ABSTAIN_CONFIDENCE = 0.5
32
+
33
+
34
+ @dataclass
35
+ class EpistemicVerdict:
36
+ status: str
37
+ confidence: float | None
38
+ reason: str
39
+ grounded: bool
40
+ ood_factor: float | None = None
41
+ out_of_scope: bool = False
42
+
43
+ def to_dict(self) -> dict:
44
+ return {"epistemic_status": self.status, "confidence": self.confidence, "reason": self.reason,
45
+ "grounded": self.grounded, "ood_factor": self.ood_factor,
46
+ "out_of_scope": self.out_of_scope}
47
+
48
+
49
+ def classify(grounded: bool, confidence: float | None = None, ood_factor: float | None = None,
50
+ abstained: bool = False, out_of_scope: bool = False,
51
+ refused: bool = False) -> EpistemicVerdict:
52
+ """Assign exactly one epistemic status from the (UQ/OOD/grounding/scope) signals.
53
+
54
+ Precedence (most-specific-wins): out-of-scope / refused / abstained → not-computable; else if grounded but
55
+ OOD-flagged or low-confidence → grounded-extrapolating; else grounded-confident. An ungrounded answer
56
+ that is none of the above is still not-computable (we never label an ungrounded number 'confident').
57
+ """
58
+ if out_of_scope:
59
+ return EpistemicVerdict(NOT_COMPUTABLE, None,
60
+ "out of scope, a known-unknown PEN-STACK does not model", grounded=False,
61
+ ood_factor=ood_factor, out_of_scope=True)
62
+ if refused or not grounded:
63
+ return EpistemicVerdict(NOT_COMPUTABLE, None,
64
+ "no validated tool can ground this value", grounded=False,
65
+ ood_factor=ood_factor)
66
+ if abstained or (confidence is not None and confidence < ABSTAIN_CONFIDENCE):
67
+ reason = ("abstained, no calibrated confidence was computed" if confidence is None
68
+ else f"abstained, confidence {confidence} below {ABSTAIN_CONFIDENCE}")
69
+ return EpistemicVerdict(NOT_COMPUTABLE, confidence, reason,
70
+ grounded=True, ood_factor=ood_factor)
71
+ if ood_factor is not None and ood_factor >= OOD_EXTRAPOLATE_FACTOR:
72
+ return EpistemicVerdict(GROUNDED_EXTRAPOLATING, confidence,
73
+ f"grounded but extrapolating, OOD factor {round(ood_factor, 3)} ≥ "
74
+ f"{OOD_EXTRAPOLATE_FACTOR} (query far from training data)",
75
+ grounded=True, ood_factor=ood_factor)
76
+ reason = ("grounded, in-distribution, calibrated" if confidence is not None
77
+ else "grounded in a validated tool output, in-distribution")
78
+ return EpistemicVerdict(GROUNDED_CONFIDENT, confidence, reason, grounded=True, ood_factor=ood_factor)
79
+
80
+
81
+ def classify_step(step_status: str, confidence: float | None = None, ood_factor: float | None = None,
82
+ out_of_scope: bool = False) -> dict:
83
+ """Map a PEN-Agent step (`ok` | `degraded` | `refused`) + UQ/OOD signals → an epistemic verdict dict.
84
+
85
+ `ok` steps are grounded; `degraded`/`refused` steps are not-computable. This is the bridge the agent
86
+ uses to tag every step (see agent.pen_agent)."""
87
+ grounded = step_status == "ok"
88
+ refused = step_status in ("refused", "degraded")
89
+ return classify(grounded=grounded, confidence=confidence, ood_factor=ood_factor,
90
+ out_of_scope=out_of_scope, refused=refused).to_dict()
91
+
92
+
93
+ def summarize(verdicts: list[dict]) -> dict:
94
+ """Roll up per-output verdicts into a session-level epistemic summary (counts per status)."""
95
+ counts = {GROUNDED_CONFIDENT: 0, GROUNDED_EXTRAPOLATING: 0, NOT_COMPUTABLE: 0}
96
+ for v in verdicts:
97
+ counts[v.get("epistemic_status", NOT_COMPUTABLE)] = counts.get(
98
+ v.get("epistemic_status", NOT_COMPUTABLE), 0) + 1
99
+ n = len(verdicts) or 1
100
+ return {"counts": counts, "n": len(verdicts),
101
+ "fraction_grounded_confident": round(counts[GROUNDED_CONFIDENT] / n, 4),
102
+ "all_tagged": all("epistemic_status" in v for v in verdicts)}
@@ -0,0 +1,67 @@
1
+ """LLM guardrails for PEN-STACK platform services.
2
+
3
+ The contract every service obeys: **grounded** (answers from the curated atlas + indexed literature),
4
+ **cited** (every factual claim carries a source), **defer-to-models** (any quantitative claim is produced
5
+ by a validated tool call, never guessed by the LLM), **decision-support** (never a clinical directive),
6
+ **budget-aware**, **auditable** (a provenance block accompanies every answer).
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import re
11
+
12
+ DISCLAIMER = ("Decision-support only - PEN-STACK returns calibrated risk/durability/reachability "
13
+ "estimates, not clinical directives. Tier-2/3 reachability is candidate and requires "
14
+ "experimental validation. Verify all designs experimentally.")
15
+
16
+ # Questions PEN-STACK must refuse: clinical directives, diagnosis, dosing, treatment decisions for a
17
+ # specific patient. (Scientific questions about loci/writers/safety are in scope.)
18
+ _REFUSE_PATTERNS = [
19
+ r"\bshould i (treat|inject|dose|administer|give)\b",
20
+ r"\b(diagnos|prescrib|dosage|dosing)\w*\b",
21
+ r"\b(my|this|the) patient\b",
22
+ r"\bdose\b.{0,40}\b(child|patient|human|person|kid|baby|infant)\b", # dosing for a person = clinical
23
+ r"\b(what|which) dose\b", # dosing questions are clinical
24
+ r"\bis it safe (to|for) (a |the |my )?(patient|human|person|child)\b",
25
+ r"\bclinical (decision|recommendation|advice) for\b",
26
+ ]
27
+
28
+
29
+ def out_of_scope(question: str) -> str | None:
30
+ """Return a refusal reason if the question is a clinical directive, else None."""
31
+ q = question.lower()
32
+ for pat in _REFUSE_PATTERNS:
33
+ if re.search(pat, q):
34
+ return ("This is a clinical-directive question. PEN-STACK is decision-support "
35
+ "infrastructure for genome-writing design and does not give clinical advice.")
36
+ return None
37
+
38
+
39
+ def check_question(question: str) -> dict | None:
40
+ """Combined gate for a free-text question. Returns a structured deferral/refusal or None.
41
+
42
+ Two distinct out-of-scope arms, both ending in zero fabrication: (1) clinical-directive REFUSAL
43
+ (`out_of_scope`), and (2) known-unknown DEFERRAL via the scope matcher (biology beyond any tool here).
44
+ Clinical refusal takes precedence (a clinical question is refused even if it also names a known-unknown).
45
+ """
46
+ clinical = out_of_scope(question)
47
+ if clinical:
48
+ return {"kind": "clinical_refusal", "epistemic_status": "not-computable", "message": clinical}
49
+ from pen_stack.agent.scope import match_scope
50
+ oos = match_scope(question)
51
+ if oos:
52
+ return {"kind": "out_of_scope", "epistemic_status": "not-computable",
53
+ "message": oos["deferral"], **oos}
54
+ return None
55
+
56
+
57
+ def enforce_grounded(answer: dict) -> dict:
58
+ """Assert the auditable contract on a finished answer: numeric claims must trace to a tool call."""
59
+ answer.setdefault("disclaimer", DISCLAIMER)
60
+ answer.setdefault("provenance", [])
61
+ answer.setdefault("citations", [])
62
+ # if the answer reports numbers, there must be a tool-call provenance entry backing them
63
+ has_number = bool(re.search(r"\d", str(answer.get("answer", ""))))
64
+ if has_number and not answer["provenance"]:
65
+ answer["warning"] = "numeric claim without tool provenance - suppressed"
66
+ answer["answer"] = "(suppressed: a number was produced without a backing tool call)"
67
+ return answer
@@ -0,0 +1,215 @@
1
+ """PEN-STACK MCP server: expose the validated capabilities to any agent.
2
+
3
+ Wraps the validated tools as a Model Context Protocol server (fastmcp) so any MCP client (Claude, etc.)
4
+ can call ``writability``, ``reachable_writers``, ``writer_axes``, ``plan_write``, ``ask_literature`` and the
5
+ grounded ``plan_write_session`` (the full PEN-Agent state machine) and receive correct, provenance-tagged
6
+ results - turning PEN-STACK into shared agentic infrastructure.
7
+
8
+ Run: ``python -m pen_stack.agent.mcp_server`` (needs the ``services`` extra: ``pip install fastmcp``).
9
+ """
10
+ from __future__ import annotations
11
+
12
+ from pen_stack.agent import pen_agent, tools
13
+
14
+ try:
15
+ from fastmcp import FastMCP
16
+ except ImportError as e: # pragma: no cover - services extra optional
17
+ raise ImportError("fastmcp not installed: pip install 'pen-stack[services]'") from e
18
+
19
+ mcp = FastMCP("pen-stack")
20
+
21
+ # register each validated tool (the same functions the in-process agent and the eval harness use)
22
+ mcp.tool()(tools.writability)
23
+ mcp.tool()(tools.reachable_writers)
24
+ mcp.tool()(tools.writer_axes)
25
+ mcp.tool()(tools.plan_write)
26
+ mcp.tool()(tools.ask_literature)
27
+ mcp.tool()(tools.multiplex_translocation_risk) # multiplex translocation-risk screen
28
+
29
+
30
+ @mcp.tool()
31
+ def plan_write_session(gene: str, intent: str, cargo_bp: int = 2000, ct: str = "k562",
32
+ payload_seq: str | None = None, mode: str = "automatic") -> dict:
33
+ """PEN-Agent: grounded write-planning state machine (site -> writer -> cargo+polish -> off-target -> 3D).
34
+
35
+ Every number is copied from a tool result with provenance; ungrounded steps degrade/refuse, never
36
+ fabricate. Modes: automatic | guided | qa."""
37
+ return pen_agent.plan_write_session(gene, intent, cargo_bp=cargo_bp, ct=ct,
38
+ payload_seq=payload_seq, mode=mode)
39
+
40
+
41
+ @mcp.tool()
42
+ def verify_write(design: dict) -> dict:
43
+ """Verifier: submit a proposed genomic write as a dict (write_type, writer_family, site_seq,
44
+ cargo_bp, delivery_vehicle, cell_type, edit_intent, no_integration, target_guide/donor_guide, edits, ...)
45
+ and get back a Verdict: legal/illegal + the named violated rule(s) + citation, a calibrated confidence on
46
+ the soft components, an epistemic status, and any out-of-scope flags. Legality and confidence are distinct
47
+ axes; every number traces to a tool (no fabrication). Unsupported write types defer."""
48
+ from pen_stack.verify import verify
49
+ return verify(design).model_dump()
50
+
51
+
52
+ @mcp.tool()
53
+ def verify_proof(design: dict) -> dict:
54
+ """Verifier: the repair-oriented proof object for a proposed write. Returns the three
55
+ axes (legality, confidence, biosecurity) reported separately, each with a status, the rule or signature
56
+ that fired, evidence, and a repair hint; the collapsed verdict is None. An agent fixes a failed-on-legality
57
+ design from the legality axis's repair hint and re-verifies. Biosecurity hazards are acknowledged and
58
+ routed to human review, never given an actionable repair."""
59
+ from pen_stack.verify.proof import verify_proof as _vp
60
+ return _vp(design).model_dump()
61
+
62
+
63
+ @mcp.tool()
64
+ def validation_campaign() -> dict:
65
+ """The validation-campaign engine. Returns the expression-validation campaign: the
66
+ next batch of (cassette x locus x cell type) measurements ordered by expected information gain, the
67
+ calibrate_axis gate it targets (the path to the program's first outcome-validated axis), and the
68
+ active-vs-random result reported verbatim. Cloud-lab-executable; Level 3, human in control; the experiments
69
+ are candidates and the wet run is the standing bottleneck."""
70
+ from pen_stack.active.campaign import design_campaign
71
+ return design_campaign()
72
+
73
+
74
+ @mcp.tool()
75
+ def cloudlab_submit(design: dict, experiment: dict | None = None, provider: str = "mock") -> dict:
76
+ """Safety-gated cloud-lab submission. The biosecurity gate runs BEFORE submission; a flagged
77
+ design returns a structured refusal (blocked=True) and NO protocol is emitted. A cleared design returns a
78
+ mock / dry-run job receipt (a real run needs a cloud-lab partner + budget). Level 3, human in control."""
79
+ from pen_stack.build.cloudlab import submit_gated
80
+ return submit_gated(design, experiment or {}, provider=provider, actor="mcp")
81
+
82
+
83
+ @mcp.tool()
84
+ def writespec_parse(prose: str, check_feasibility: bool = True) -> dict:
85
+ """Parse a plain-language genome-writing request into a typed, ontology-backed WriteSpec (an
86
+ SBOL3 profile). Returns the typed spec with per-field provenance (explicit / inferred / user / unresolved),
87
+ the assumptions behind every inferred field, clarifying questions for anything underspecified or ambiguous,
88
+ the unresolved terms (kept null, never invented), the downstream design adapter, and a feasibility verdict
89
+ (reachability + deliverability + legality, with named blocking constraints + repair hints). A WriteSpec is a
90
+ REQUEST, not a claim; the extractor never fabricates intent."""
91
+ from pen_stack.spec.service import parse_request
92
+ return parse_request(prose, check_feasibility=check_feasibility)
93
+
94
+
95
+ @mcp.tool()
96
+ def oracle_query(oracle: str | None = None, protein_seq: str | None = None, ligand_smiles: str | None = None,
97
+ pair_type: str = "ligand", ligand_name: str | None = None) -> dict:
98
+ """Query the oracle mesh under one contract. With no arguments, returns every oracle's
99
+ execution + latency + live status + PUBLISHED reliability (reported verbatim from public benchmarks, with
100
+ citation; not a claim about this stack's accuracy) and the disagreement-to-interval check. With `oracle` set,
101
+ returns that oracle's status + reliability. With `protein_seq` + `ligand_smiles`, returns a CANDIDATE
102
+ binding-affinity prediction (Boltz-2 head): a binder probability + a predicted value with native uncertainty,
103
+ cache-or-abstain; protein-protein/protein-DNA pair types are flagged extrapolating (the head is
104
+ protein-ligand only). The long GPU job never runs on the request path."""
105
+ from pen_stack.oracles.status import oracle_status, summary
106
+ if protein_seq and ligand_smiles:
107
+ from pen_stack.oracles.affinity import predict_affinity
108
+ return predict_affinity(protein_seq, ligand_smiles, pair_type=pair_type, ligand_name=ligand_name).model_dump()
109
+ st = oracle_status()
110
+ if oracle:
111
+ return {"oracle": oracle, "status": st.get(oracle), "found": oracle in st}
112
+ return {"summary": summary(), "oracles": st}
113
+
114
+
115
+ @mcp.tool()
116
+ def graph_query(locus: str, cargo_form: str | None = None) -> dict:
117
+ """World-model graph: a multi-hop query. Returns the writer families that REACH `locus` AND
118
+ are DELIVERABLE by a vehicle carrying `cargo_form` (optional), each answer with its provenanced edge path
119
+ (the answer IS the path, no fabrication). The graph nodes/edges carry evidence kind + scope + provenance."""
120
+ from pen_stack.graph import writers_reaching_and_deliverable
121
+ return writers_reaching_and_deliverable(locus, cargo_form=cargo_form)
122
+
123
+
124
+ # ---- the AI Integration Surface: self-describing resources + the engine tools ----
125
+ @mcp.resource("pen-stack://capabilities")
126
+ def capabilities_resource() -> dict:
127
+ """WHAT PEN-STACK can do (machine-readable). An agent reads this and routes on it, not on prose."""
128
+ from pen_stack.api.manifest import capability_manifest
129
+ return capability_manifest()
130
+
131
+
132
+ @mcp.resource("pen-stack://scope")
133
+ def scope_resource() -> dict:
134
+ """WHAT PEN-STACK REFUSES to answer: the known-unknowns + the oracle scope cards. The contract that makes
135
+ depending on PEN-STACK safe, outputs outside scope are out_of_scope/extrapolating, never asserted."""
136
+ from pen_stack.api.manifest import scope_manifest
137
+ return scope_manifest()
138
+
139
+
140
+ @mcp.tool()
141
+ def safety_screen(design: dict) -> dict:
142
+ """Guardian: biosecurity / dual-use screen -> SafetyVerdict (clear/flag/escalate/refuse) + reason. A
143
+ hazardous design returns a STRUCTURED refusal an agent can branch on (decision == 'refuse')."""
144
+ from pen_stack.safety import safety_gate
145
+ return safety_gate(design, actor=str(design.get("actor", "mcp"))).model_dump()
146
+
147
+
148
+ @mcp.tool()
149
+ def immune_profile(design: dict) -> dict:
150
+ """Immune-risk profile: a per-axis screen (genotox/CD8/innate/NAb/anti-PEG), each with its own
151
+ uncertainty + validation label. Never collapsed into one number (collapsed_score is None); magnitude is a
152
+ declared known-unknown."""
153
+ from pen_stack.planner.immune_profile import immune_profile as _ip
154
+ return _ip(design)
155
+
156
+
157
+ @mcp.tool()
158
+ def offtarget_scan(writer_family: str, guide: str | None = None, candidate_sites: list | None = None,
159
+ sequence: str | None = None, assay: str = "guideseq", enzyme: str | None = None,
160
+ max_mismatch: int = 5) -> dict:
161
+ """Genome-wide, per-mechanism off-target FINDER (NOT a clearance). For a nuclease guide it
162
+ enumerates the genome-wide off-target set over GRCh38 (Cas-OFFinder, cached) + real CRISOT + risk + chromatin
163
+ (validated); serine integrase = genome-wide pseudo-attP scan (semi-validated); bridge = DMS-scored scan
164
+ (mechanism-based, unvalidated); CAST = guide-directed + untargeted-transposition background (unvalidated);
165
+ PASTE = nuclease + integrase composition. Each carries a truthful status label + the confirming assay.
166
+ Abstains for a novel input (VM scan); never fabricates sites."""
167
+ from pen_stack.wgenome.offtarget_predict import nominate_offtargets
168
+ return nominate_offtargets(writer_family, guide=guide, candidate_sites=candidate_sites,
169
+ sequence=sequence, assay=assay, enzyme=enzyme, max_mismatch=max_mismatch)
170
+
171
+
172
+ @mcp.tool()
173
+ def delivery_recommend(cargo_form: str, cargo_bp: int | None = None, target_tissue: str | None = None,
174
+ safety_weight: float = 0.5, in_vivo: bool | None = None) -> dict:
175
+ """Cross-modality delivery recommender: rank vehicles by cargo-form + safety<->efficacy + a
176
+ GROUNDED serotype->tissue tropism prior (from approved AAV therapies; a known-unknown for novel capsids), plus
177
+ the learned FLIP-AAV capsid-fitness capability. Never fabricates tropism; abstains without inputs."""
178
+ from pen_stack.planner.delivery_predict import recommend_delivery_plus
179
+ return recommend_delivery_plus(cargo_form, cargo_bp, target_tissue, safety_weight=safety_weight, in_vivo=in_vivo)
180
+
181
+
182
+ @mcp.tool()
183
+ def generate_designs(goal: dict | None = None, candidates: list | None = None, keep: int = 25) -> dict:
184
+ """Generative designer (verifier-as-discriminator): hazardous/illegal candidates are DISCARDED;
185
+ survivors are calibrated + immune-profiled CANDIDATES (never asserted to work)."""
186
+ from pen_stack.design import generate_designs as _gd
187
+ return {"survivors": _gd(goal, candidates=candidates, keep=keep, actor="mcp")}
188
+
189
+
190
+ @mcp.tool()
191
+ def predict_outcome(design: dict, cell_state: str = "k562") -> dict:
192
+ """Digital twin: a calibrated, OOD-gated, phenotype-bounded outcome (interval + scope flags). A
193
+ candidate prediction, never the truth; phenotype/in-vivo magnitude stay out of scope."""
194
+ from pen_stack.twin import predict_outcome as _po
195
+ return _po(design, cell_state)
196
+
197
+
198
+ @mcp.tool()
199
+ def suggest_experiment(candidates: list, cell_state: str = "k562", k: int = 8) -> dict:
200
+ """Experiment designer: a diverse, informative next-experiment batch (EIG + immune-VOI), each with
201
+ its expected information gain."""
202
+ from pen_stack.active import select_batch
203
+ return {"batch": select_batch(candidates, cell_state, {}, k=k)}
204
+
205
+
206
+ @mcp.tool()
207
+ def co_scientist_session(goal: dict, cell_state: str = "k562") -> dict:
208
+ """Co-scientist: drive the full loop -> Pareto strategies + calibrated outcomes + per-axis immune
209
+ profiles + suggested experiments + citations + scope ledger + safety. The scientist decides; this drives."""
210
+ from pen_stack.agent.co_scientist import co_scientist_session as _cs
211
+ return _cs(goal, cell_state)
212
+
213
+
214
+ if __name__ == "__main__": # pragma: no cover
215
+ mcp.run()