coredipper/operon-lifecycle
0
1"""2Operon Lifecycle Manager -- Telomere & Genome Demo3===================================================4 5Two-tab demo for agent lifecycle management:6 71. Telomere Lifecycle: Watch telomeres shorten as operations execute,8 phase transitions, and optional renewal.92. Genome: Configure genes, express active config, replicate with mutations.10 11Run locally:12 pip install gradio13 python space-lifecycle/app.py14"""15 16import sys17from pathlib import Path18 19import gradio as gr20 21_repo_root = Path(__file__).resolve().parent.parent22if str(_repo_root) not in sys.path:23 sys.path.insert(0, str(_repo_root))24 25from operon_ai import (26 Genome,27 Gene,28 GeneType,29 Telomere,30 TelomereStatus,31 LifecyclePhase,32)33 34 35# ---------------------------------------------------------------------------36# Telomere presets37# ---------------------------------------------------------------------------38 39TELOMERE_PRESETS: dict[str, dict] = {40 "(custom)": {41 "max_ops": 100, "error_threshold": 10, "cost": 1, "allow_renewal": False,42 "description": "Configure your own parameters",43 },44 "Long-lived agent": {45 "max_ops": 200, "error_threshold": 20, "cost": 1, "allow_renewal": False,46 "description": "High capacity agent -- slow telomere depletion",47 },48 "Fragile agent": {49 "max_ops": 30, "error_threshold": 3, "cost": 1, "allow_renewal": False,50 "description": "Low capacity -- enters senescence quickly",51 },52 "Error-prone agent": {53 "max_ops": 100, "error_threshold": 5, "cost": 1, "allow_renewal": False,54 "description": "Errors injected every 10 ops -- tests error accumulation",55 },56 "Renewable agent": {57 "max_ops": 50, "error_threshold": 10, "cost": 2, "allow_renewal": True,58 "description": "Renewal enabled -- telomeres extend when senescent",59 },60}61 62 63# ---------------------------------------------------------------------------64# Genome presets65# ---------------------------------------------------------------------------66 67GENOME_PRESETS: dict[str, list[dict]] = {68 "(custom)": [],69 "Worker agent": [70 {"name": "model", "value": "gpt-4", "type": "STRUCTURAL"},71 {"name": "temperature", "value": "0.7", "type": "REGULATORY"},72 {"name": "max_tokens", "value": "4096", "type": "STRUCTURAL"},73 {"name": "retries", "value": "3", "type": "HOUSEKEEPING"},74 {"name": "debug", "value": "False", "type": "DORMANT"},75 ],76 "Creative agent": [77 {"name": "model", "value": "gpt-4", "type": "STRUCTURAL"},78 {"name": "temperature", "value": "1.2", "type": "REGULATORY"},79 {"name": "creativity", "value": "0.9", "type": "REGULATORY"},80 {"name": "max_tokens", "value": "8192", "type": "STRUCTURAL"},81 {"name": "experimental", "value": "True", "type": "CONDITIONAL"},82 ],83 "Safety-first": [84 {"name": "model", "value": "gpt-4", "type": "STRUCTURAL"},85 {"name": "safety_checks", "value": "True", "type": "STRUCTURAL"},86 {"name": "temperature", "value": "0.3", "type": "REGULATORY"},87 {"name": "experimental", "value": "False", "type": "DORMANT"},88 {"name": "audit_log", "value": "True", "type": "HOUSEKEEPING"},89 ],90}91 92 93# ---------------------------------------------------------------------------94# Styling95# ---------------------------------------------------------------------------96 97PHASE_STYLES = {98 LifecyclePhase.NASCENT: ("#94a3b8", "NASCENT", "Initializing"),99 LifecyclePhase.ACTIVE: ("#22c55e", "ACTIVE", "Normal operation"),100 LifecyclePhase.SENESCENT: ("#f59e0b", "SENESCENT", "Aging, reduced capability"),101 LifecyclePhase.APOPTOTIC: ("#ef4444", "APOPTOTIC", "Preparing for shutdown"),102 LifecyclePhase.TERMINATED: ("#6b7280", "TERMINATED", "No longer operational"),103}104 105GENE_TYPE_MAP = {106 "STRUCTURAL": GeneType.STRUCTURAL,107 "REGULATORY": GeneType.REGULATORY,108 "HOUSEKEEPING": GeneType.HOUSEKEEPING,109 "CONDITIONAL": GeneType.CONDITIONAL,110 "DORMANT": GeneType.DORMANT,111}112 113 114def _phase_badge(phase: LifecyclePhase) -> str:115 color, label, _ = PHASE_STYLES.get(phase, ("#6b7280", "UNKNOWN", ""))116 return (117 f'<span style="background:{color};color:white;padding:2px 8px;'118 f'border-radius:4px;font-size:0.85em;font-weight:600;">{label}</span>'119 )120 121 122def _telomere_bar(current: int, maximum: int) -> str:123 pct = max(0, min(100, int(current / maximum * 100))) if maximum > 0 else 0124 if pct > 50:125 color = "#22c55e"126 elif pct > 20:127 color = "#f59e0b"128 else:129 color = "#ef4444"130 return (131 f'<div style="margin:8px 0;">'132 f'<div style="display:flex;justify-content:space-between;font-size:0.85em;">'133 f'<span>Telomere Length</span><span>{current}/{maximum}</span></div>'134 f'<div style="background:#e5e7eb;border-radius:4px;height:20px;">'135 f'<div style="width:{pct}%;background:{color};height:100%;border-radius:4px;'136 f'transition:width 0.3s;"></div></div></div>'137 )138 139 140# ---------------------------------------------------------------------------141# Telomere logic142# ---------------------------------------------------------------------------143 144def run_telomere(145 preset_name: str,146 max_ops: int,147 error_threshold: int,148 cost_per_op: int,149 allow_renewal: bool,150) -> tuple[str, str, str, str]:151 """Run the telomere lifecycle simulation.152 153 Returns (summary_html, telomere_bar_html, timeline_md, events_md).154 """155 max_ops = int(max_ops)156 error_threshold = int(error_threshold)157 cost_per_op = int(cost_per_op)158 is_error_prone = preset_name == "Error-prone agent"159 160 telomere = Telomere(161 max_operations=max_ops,162 error_threshold=error_threshold,163 allow_renewal=allow_renewal,164 silent=True,165 )166 telomere.start()167 168 timeline_rows = []169 phase_transitions = []170 prev_phase = telomere.get_phase()171 renewed = False172 173 step = 0174 while telomere.is_operational():175 step += 1176 177 # Inject errors for error-prone preset178 if is_error_prone and step % 10 == 0:179 telomere.record_error()180 status = telomere.get_status()181 timeline_rows.append({182 "step": step,183 "action": "ERROR",184 "length": status.telomere_length,185 "remaining": status.operations_remaining,186 "health": status.health_score,187 "phase": status.phase,188 })189 new_phase = status.phase190 if new_phase != prev_phase:191 phase_transitions.append((step, prev_phase, new_phase))192 prev_phase = new_phase193 if not telomere.is_operational():194 break195 continue196 197 can_continue = telomere.tick(cost=cost_per_op)198 status = telomere.get_status()199 200 new_phase = status.phase201 if new_phase != prev_phase:202 phase_transitions.append((step, prev_phase, new_phase))203 prev_phase = new_phase204 205 timeline_rows.append({206 "step": step,207 "action": "TICK",208 "length": status.telomere_length,209 "remaining": status.operations_remaining,210 "health": status.health_score,211 "phase": status.phase,212 })213 214 # Renewal when senescent215 if allow_renewal and not renewed and new_phase == LifecyclePhase.SENESCENT:216 telomere.renew()217 renewed = True218 status = telomere.get_status()219 new_phase = status.phase220 if new_phase != prev_phase:221 phase_transitions.append((step, prev_phase, new_phase))222 prev_phase = new_phase223 timeline_rows.append({224 "step": step,225 "action": "RENEW",226 "length": status.telomere_length,227 "remaining": status.operations_remaining,228 "health": status.health_score,229 "phase": status.phase,230 })231 232 if not can_continue:233 break234 235 # Safety cap236 if step > max_ops + 50:237 break238 239 # Final status240 final_status = telomere.get_status()241 stats = telomere.get_statistics()242 243 # --- Summary banner ---244 phase_color, _, phase_desc = PHASE_STYLES.get(245 final_status.phase, ("#6b7280", "UNKNOWN", "")246 )247 summary_html = (248 f'<div style="padding:16px;border-radius:8px;border:2px solid {phase_color};background:#f9fafb;">'249 f'<div style="display:flex;align-items:center;gap:12px;margin-bottom:8px;">'250 f'<span style="font-size:1.2em;font-weight:700;">Final Phase:</span>'251 f'{_phase_badge(final_status.phase)}'252 f'<span style="color:#6b7280;font-size:0.9em;">-- {phase_desc}</span>'253 f'</div>'254 f'<div style="display:flex;gap:20px;font-size:0.9em;flex-wrap:wrap;">'255 f'<span>Operations: <b>{step}</b></span>'256 f'<span>Health: <b>{final_status.health_score:.0%}</b></span>'257 f'<span>Telomere: <b>{final_status.telomere_length}/{final_status.max_telomere_length}</b></span>'258 f'</div>'259 f'</div>'260 )261 262 # --- Telomere bar ---263 bar_html = _telomere_bar(final_status.telomere_length, final_status.max_telomere_length)264 265 # --- Timeline table (sample every N rows if large) ---266 sample_interval = max(1, len(timeline_rows) // 30)267 timeline_md = "| Step | Action | Telomere | Remaining | Health | Phase |\n"268 timeline_md += "|------|--------|----------|-----------|--------|-------|\n"269 for i, row in enumerate(timeline_rows):270 if i % sample_interval == 0 or i == len(timeline_rows) - 1 or row["action"] in ("RENEW", "ERROR"):271 timeline_md += (272 f'| {row["step"]} | {row["action"]} | {row["length"]} '273 f'| {row["remaining"]} | {row["health"]:.0%} '274 f'| {_phase_badge(row["phase"])} |\n'275 )276 277 if phase_transitions:278 timeline_md += "\n**Phase transitions:**\n\n"279 for step_num, old, new in phase_transitions:280 _, old_label, _ = PHASE_STYLES.get(old, ("#6b7280", "?", ""))281 _, new_label, _ = PHASE_STYLES.get(new, ("#6b7280", "?", ""))282 timeline_md += f"- Step {step_num}: {old_label} -> {new_label}\n"283 284 # --- Events ---285 events_md = "### Lifecycle Events\n\n"286 events = telomere.get_events(limit=20)287 if events:288 events_md += "| Event | Details |\n"289 events_md += "|-------|---------|\n"290 for ev in events:291 details_str = ", ".join(f"{k}={v}" for k, v in ev.details.items()) if ev.details else "--"292 events_md += f"| {ev.event_type} | {details_str} |\n"293 else:294 events_md += "*No events recorded.*\n"295 296 return summary_html, bar_html, timeline_md, events_md297 298 299def load_telomere_preset(name: str):300 preset = TELOMERE_PRESETS.get(name)301 if not preset:302 return 100, 10, 1, False303 return preset["max_ops"], preset["error_threshold"], preset["cost"], preset["allow_renewal"]304 305 306# ---------------------------------------------------------------------------307# Genome logic308# ---------------------------------------------------------------------------309 310def _parse_gene_value(value_str: str):311 """Parse a string into a typed value."""312 if value_str.lower() == "true":313 return True314 if value_str.lower() == "false":315 return False316 try:317 return int(value_str)318 except ValueError:319 pass320 try:321 return float(value_str)322 except ValueError:323 pass324 return value_str325 326 327def run_genome_express(328 name1, val1, type1,329 name2, val2, type2,330 name3, val3, type3,331 name4, val4, type4,332 name5, val5, type5,333) -> tuple[str, str]:334 """Express a genome and show active config.335 336 Returns (config_html, stats_md).337 """338 names = [name1, name2, name3, name4, name5]339 values = [val1, val2, val3, val4, val5]340 types = [type1, type2, type3, type4, type5]341 342 genes = []343 for name, val, gtype in zip(names, values, types):344 if not name.strip():345 continue346 gene_type = GENE_TYPE_MAP.get(gtype, GeneType.STRUCTURAL)347 genes.append(Gene(348 name=name.strip(),349 value=_parse_gene_value(val.strip()),350 gene_type=gene_type,351 ))352 353 if not genes:354 return "Add at least one gene.", ""355 356 genome = Genome(genes=genes, allow_mutations=True, silent=True)357 expressed = genome.express()358 359 # --- Config display ---360 config_html = (361 '<div style="padding:16px;border-radius:8px;border:2px solid #22c55e;background:#f0fdf4;">'362 '<div style="font-size:1.2em;font-weight:700;color:#16a34a;margin-bottom:8px;">'363 'Expressed Configuration</div>'364 )365 for key, value in expressed.items():366 config_html += (367 f'<div style="font-family:monospace;font-size:0.95em;padding:2px 0;">'368 f'<span style="color:#6b7280;">{key}:</span> '369 f'<span style="color:#15803d;font-weight:600;">{value}</span></div>'370 )371 config_html += f'<div style="margin-top:8px;font-size:0.8em;color:#6b7280;">Genome hash: <code>{genome.get_hash()}</code></div>'372 config_html += '</div>'373 374 # --- Stats ---375 stats = genome.get_statistics()376 gene_list = genome.list_genes()377 378 stats_md = "### Gene Details\n\n"379 stats_md += "| Name | Value | Type | Expression |\n"380 stats_md += "|------|-------|------|------------|\n"381 for g in gene_list:382 stats_md += f"| {g['name']} | {g['value']} | {g['type']} | {g['expression']} |\n"383 384 stats_md += f"\n**Total genes:** {stats['total_genes']}\n\n"385 stats_md += f"**Generation:** {stats['generation']}\n\n"386 stats_md += f"**Genome hash:** `{genome.get_hash()}`\n"387 388 return config_html, stats_md389 390 391def run_genome_replicate(392 name1, val1, type1,393 name2, val2, type2,394 name3, val3, type3,395 name4, val4, type4,396 name5, val5, type5,397) -> tuple[str, str]:398 """Replicate genome with mutations and show diff.399 400 Returns (diff_html, details_md).401 """402 names = [name1, name2, name3, name4, name5]403 values = [val1, val2, val3, val4, val5]404 types = [type1, type2, type3, type4, type5]405 406 genes = []407 for name, val, gtype in zip(names, values, types):408 if not name.strip():409 continue410 gene_type = GENE_TYPE_MAP.get(gtype, GeneType.STRUCTURAL)411 genes.append(Gene(412 name=name.strip(),413 value=_parse_gene_value(val.strip()),414 gene_type=gene_type,415 ))416 417 if not genes:418 return "Add at least one gene.", ""419 420 parent = Genome(genes=genes, allow_mutations=True, silent=True)421 422 # Create mutations: modify first REGULATORY gene's value423 mutations = {}424 for g in genes:425 if g.gene_type == GeneType.REGULATORY:426 if isinstance(g.value, (int, float)):427 mutations[g.name] = round(g.value * 1.5, 2)428 elif isinstance(g.value, bool):429 mutations[g.name] = not g.value430 else:431 mutations[g.name] = g.value + "_mutated"432 break433 434 if not mutations:435 # Mutate first gene if no regulatory found436 g = genes[0]437 if isinstance(g.value, (int, float)):438 mutations[g.name] = round(g.value * 2, 2)439 else:440 mutations[g.name] = str(g.value) + "_v2"441 442 child = parent.replicate(mutations=mutations)443 444 diff = parent.diff(child)445 446 # --- Diff display ---447 diff_html = (448 '<div style="padding:16px;border-radius:8px;border:2px solid #8b5cf6;background:#f5f3ff;">'449 '<div style="font-size:1.2em;font-weight:700;color:#7c3aed;margin-bottom:8px;">'450 'Replication Diff</div>'451 )452 if diff:453 for gene_name, (parent_val, child_val) in diff.items():454 diff_html += (455 f'<div style="font-family:monospace;font-size:0.95em;padding:4px 0;">'456 f'<span style="color:#6b7280;">{gene_name}:</span> '457 f'<span style="color:#dc2626;text-decoration:line-through;">{parent_val}</span> '458 f'-> <span style="color:#16a34a;font-weight:600;">{child_val}</span></div>'459 )460 else:461 diff_html += '<div style="color:#6b7280;">No differences found.</div>'462 463 diff_html += (464 f'<div style="margin-top:8px;font-size:0.8em;color:#6b7280;">'465 f'Parent hash: <code>{parent.get_hash()}</code> | '466 f'Child hash: <code>{child.get_hash()}</code></div>'467 )468 diff_html += '</div>'469 470 # --- Details ---471 parent_expressed = parent.express()472 child_expressed = child.express()473 474 details_md = "### Comparison\n\n"475 details_md += "| Gene | Parent | Child | Changed |\n"476 details_md += "|------|--------|-------|---------|\n"477 all_keys = set(list(parent_expressed.keys()) + list(child_expressed.keys()))478 for key in sorted(all_keys):479 pv = parent_expressed.get(key, "--")480 cv = child_expressed.get(key, "--")481 changed = "Yes" if pv != cv else ""482 details_md += f"| {key} | {pv} | {cv} | {changed} |\n"483 484 details_md += f"\n**Mutations applied:** {mutations}\n"485 486 return diff_html, details_md487 488 489def load_genome_preset(name: str):490 """Load a genome preset into the gene fields."""491 preset = GENOME_PRESETS.get(name, [])492 result = []493 for i in range(5):494 if i < len(preset):495 result.extend([preset[i]["name"], preset[i]["value"], preset[i]["type"]])496 else:497 result.extend(["", "", "STRUCTURAL"])498 return result499 500 501# ---------------------------------------------------------------------------502# Gradio UI503# ---------------------------------------------------------------------------504 505def build_app() -> gr.Blocks:506 gene_type_choices = list(GENE_TYPE_MAP.keys())507 508 with gr.Blocks(title="Operon Lifecycle Manager") as app:509 gr.Markdown(510 "# Operon Lifecycle Manager\n"511 "Agent lifecycle management with biological **telomere shortening** "512 "and **genome configuration**.\n\n"513 "[GitHub](https://github.com/coredipper/operon) | "514 "[Paper](https://github.com/coredipper/operon/tree/main/article)"515 )516 517 with gr.Tabs():518 # --- Telomere Tab ---519 with gr.TabItem("Telomere Lifecycle"):520 gr.Markdown(521 "### Telomere Shortening Simulation\n\n"522 "Watch how an agent's telomeres shorten with each operation. "523 "When telomeres deplete, the agent enters senescence. "524 "With renewal enabled, telomeres can be extended."525 )526 527 with gr.Row():528 telo_preset = gr.Dropdown(529 choices=list(TELOMERE_PRESETS.keys()),530 value="(custom)",531 label="Load Preset",532 scale=2,533 )534 telo_run_btn = gr.Button("Run Lifecycle", variant="primary", scale=1)535 536 with gr.Row():537 max_ops_slider = gr.Slider(538 minimum=10, maximum=300, value=100, step=10,539 label="Max Operations",540 )541 error_thresh_slider = gr.Slider(542 minimum=1, maximum=50, value=10, step=1,543 label="Error Threshold",544 )545 cost_slider = gr.Slider(546 minimum=1, maximum=10, value=1, step=1,547 label="Cost per Operation",548 )549 renewal_check = gr.Checkbox(550 label="Allow Renewal",551 value=False,552 )553 554 telo_summary = gr.HTML(label="Summary")555 telo_bar = gr.HTML(label="Telomere")556 557 with gr.Row():558 with gr.Column(scale=2):559 gr.Markdown("### Timeline")560 telo_timeline = gr.Markdown()561 with gr.Column(scale=1):562 telo_events = gr.Markdown()563 564 telo_run_btn.click(565 fn=run_telomere,566 inputs=[telo_preset, max_ops_slider, error_thresh_slider, cost_slider, renewal_check],567 outputs=[telo_summary, telo_bar, telo_timeline, telo_events],568 )569 telo_preset.change(570 fn=load_telomere_preset,571 inputs=[telo_preset],572 outputs=[max_ops_slider, error_thresh_slider, cost_slider, renewal_check],573 )574 575 # --- Genome Tab ---576 with gr.TabItem("Genome"):577 gr.Markdown(578 "### Genome Configuration\n\n"579 "Configure agent genes with types: STRUCTURAL (core), "580 "REGULATORY (controls), HOUSEKEEPING (essential), "581 "CONDITIONAL (context-dependent), DORMANT (inactive)."582 )583 584 genome_preset = gr.Dropdown(585 choices=list(GENOME_PRESETS.keys()),586 value="(custom)",587 label="Load Preset",588 )589 590 gene_components = []591 for i in range(5):592 with gr.Row():593 gname = gr.Textbox(label=f"Gene {i+1} Name", value="", scale=2)594 gval = gr.Textbox(label="Value", value="", scale=2)595 gtype = gr.Dropdown(596 choices=gene_type_choices,597 value="STRUCTURAL",598 label="Type",599 scale=1,600 )601 gene_components.extend([gname, gval, gtype])602 603 with gr.Row():604 express_btn = gr.Button("Express", variant="primary")605 replicate_btn = gr.Button("Replicate with Mutations", variant="secondary")606 607 genome_config = gr.HTML(label="Configuration")608 genome_stats = gr.Markdown()609 610 express_btn.click(611 fn=run_genome_express,612 inputs=gene_components,613 outputs=[genome_config, genome_stats],614 )615 replicate_btn.click(616 fn=run_genome_replicate,617 inputs=gene_components,618 outputs=[genome_config, genome_stats],619 )620 genome_preset.change(621 fn=load_genome_preset,622 inputs=[genome_preset],623 outputs=gene_components,624 )625 626 return app627 628 629if __name__ == "__main__":630 app = build_app()631 app.launch(theme=gr.themes.Soft())632 