2026-05-27 20:01:51 +02:00
|
|
|
|
"""Assistance orchestrator.
|
2026-05-26 02:19:13 +02:00
|
|
|
|
|
|
|
|
|
|
The piece that turns raw user intent + collected context into a well-formed
|
2026-05-27 20:01:51 +02:00
|
|
|
|
request for the LLM adapter, then turns the adapter response into the
|
2026-05-26 02:19:13 +02:00
|
|
|
|
final terminal output the user sees.
|
|
|
|
|
|
|
2026-05-27 20:01:51 +02:00
|
|
|
|
Key responsibilities:
|
|
|
|
|
|
- Coordinate context collector, memory (Profile 0 via recall with activation_context),
|
|
|
|
|
|
rule-based risk classifier, and LLMAdapter.
|
|
|
|
|
|
- Wire memory influence into every assistance cycle and `--explain-context`.
|
|
|
|
|
|
- Own the `cya retrospect` flow that feeds higher-order memory back into the system.
|
|
|
|
|
|
- Keep the CLI surface thin.
|
|
|
|
|
|
|
|
|
|
|
|
**Memory note (Profile 0):** This module is the primary consumer of the
|
|
|
|
|
|
stable memory ports defined in src/cya/memory/__init__.py. All calls use
|
|
|
|
|
|
activation_context derived from the current working directory + git root.
|
|
|
|
|
|
See MemoryVision.md "Profile 0 Baseline" and CYA-WP-0005 for the evolution
|
|
|
|
|
|
path to Profiles 1–3.
|
|
|
|
|
|
|
|
|
|
|
|
See workplan CYA-WP-0001 (core) + CYA-WP-0003 (memory wiring + retrospect)
|
|
|
|
|
|
+ CYA-WP-0005 (profile formalization).
|
2026-05-26 02:19:13 +02:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
2026-05-26 15:18:46 +02:00
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
2026-06-22 01:39:07 +02:00
|
|
|
|
import typer
|
2026-05-26 02:19:13 +02:00
|
|
|
|
from rich.console import Console
|
|
|
|
|
|
from rich.panel import Panel
|
|
|
|
|
|
|
|
|
|
|
|
from cya.context.collector import collect, render_explanation
|
2026-05-26 15:26:54 +02:00
|
|
|
|
from cya.memory import (
|
|
|
|
|
|
recall_preferences,
|
|
|
|
|
|
remember_retrospection_outcome,
|
|
|
|
|
|
KIND_RETROSPECTION,
|
|
|
|
|
|
KIND_INTERACTION_GOAL,
|
2026-05-28 03:24:44 +02:00
|
|
|
|
KIND_REFLECTION,
|
2026-05-26 15:26:54 +02:00
|
|
|
|
)
|
2026-06-22 01:39:07 +02:00
|
|
|
|
from cya.memory.reflections import (
|
|
|
|
|
|
REFLECTION_CAPTURE_PROMPTS,
|
|
|
|
|
|
collect_lessons_from_answers,
|
|
|
|
|
|
compact_reflections,
|
|
|
|
|
|
find_duplicate_reflection_groups,
|
|
|
|
|
|
format_reflection_surfacing,
|
|
|
|
|
|
preview_lessons,
|
|
|
|
|
|
save_reflection_lessons,
|
|
|
|
|
|
session_provenance,
|
|
|
|
|
|
)
|
2026-05-26 02:19:13 +02:00
|
|
|
|
from cya.safety.risk import classify, get_user_confirmation
|
2026-06-22 10:36:10 +02:00
|
|
|
|
from cya.config import bound_session_turns
|
|
|
|
|
|
from cya.llm.adapter import AssistanceRequest
|
|
|
|
|
|
from cya.llm.factory import get_adapter
|
2026-05-26 02:19:13 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
console = Console()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def handle_request(
|
|
|
|
|
|
user_request: str,
|
|
|
|
|
|
*,
|
|
|
|
|
|
explain_context: bool = False,
|
|
|
|
|
|
dry_run: bool = False,
|
2026-06-22 10:36:10 +02:00
|
|
|
|
offline: bool = False,
|
|
|
|
|
|
session_turns: list[dict[str, str]] | None = None,
|
2026-05-26 02:19:13 +02:00
|
|
|
|
) -> None:
|
|
|
|
|
|
"""Primary orchestrator entry point.
|
|
|
|
|
|
|
|
|
|
|
|
This is what the CLI (and future tests / other front-ends) should call.
|
|
|
|
|
|
It coordinates the full current flow:
|
|
|
|
|
|
context → safety (with mandatory confirmation) → LLMAdapter → render
|
|
|
|
|
|
"""
|
|
|
|
|
|
# 1. Context (always cheap; needed for safety "affected" and for the adapter)
|
|
|
|
|
|
try:
|
|
|
|
|
|
envelope = collect(".")
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
envelope = None
|
|
|
|
|
|
|
|
|
|
|
|
if explain_context and envelope:
|
|
|
|
|
|
try:
|
|
|
|
|
|
explanation = render_explanation(envelope)
|
|
|
|
|
|
console.print(
|
|
|
|
|
|
Panel(
|
|
|
|
|
|
explanation,
|
|
|
|
|
|
title="Context Envelope (T02)",
|
|
|
|
|
|
border_style="green",
|
|
|
|
|
|
padding=(1, 1),
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
console.print(f"[red]Context explanation error: {exc}[/red]")
|
|
|
|
|
|
|
2026-05-26 03:13:06 +02:00
|
|
|
|
# T03 (memory wiring): consult after context (so safety can see it in future T04 0002),
|
|
|
|
|
|
# before risk/LLM. Real T02 prefs now available; graceful.
|
2026-05-26 15:18:46 +02:00
|
|
|
|
# T03 (0003): pass activation_context so directory/project-bound memory is automatically
|
|
|
|
|
|
# activated based on cwd + git root.
|
2026-05-26 03:13:06 +02:00
|
|
|
|
memory = {}
|
|
|
|
|
|
try:
|
2026-05-26 15:18:46 +02:00
|
|
|
|
act_ctx = {"cwd": str(Path.cwd())}
|
|
|
|
|
|
if envelope and getattr(envelope, "git", None):
|
|
|
|
|
|
git_info = envelope.git or {}
|
|
|
|
|
|
if git_info.get("workdir"):
|
|
|
|
|
|
act_ctx["git_root"] = git_info["workdir"]
|
2026-05-28 03:24:44 +02:00
|
|
|
|
memory = recall_preferences(
|
|
|
|
|
|
".",
|
|
|
|
|
|
activation_context=act_ctx,
|
|
|
|
|
|
kinds=[KIND_REFLECTION, KIND_RETROSPECTION, KIND_INTERACTION_GOAL, "preference"],
|
|
|
|
|
|
)
|
2026-05-26 03:13:06 +02:00
|
|
|
|
except Exception:
|
|
|
|
|
|
memory = {"error": "recall failed (graceful degradation)"}
|
|
|
|
|
|
|
|
|
|
|
|
if explain_context and memory.get("items"):
|
|
|
|
|
|
try:
|
|
|
|
|
|
prov = memory.get("provenance", [{}])[0]
|
2026-05-26 15:18:46 +02:00
|
|
|
|
sample = ", ".join(i.get("key", "?") for i in memory.get("items", [])[:3])
|
|
|
|
|
|
act_note = ""
|
|
|
|
|
|
if prov.get("activation_context"):
|
|
|
|
|
|
act_note = f" | ctx: {prov['activation_context']}"
|
2026-06-22 01:39:07 +02:00
|
|
|
|
body = (
|
|
|
|
|
|
f"Phase: {memory.get('phase')} | {len(memory.get('items', []))} items | "
|
|
|
|
|
|
f"{prov.get('source', 'local')}{act_note}\n"
|
|
|
|
|
|
f"Sample activated: {sample}"
|
|
|
|
|
|
)
|
|
|
|
|
|
refl_line = format_reflection_surfacing(memory, for_explain=True)
|
|
|
|
|
|
if refl_line:
|
|
|
|
|
|
body += f"\n\n[cyan]Reflections:[/cyan] {refl_line}"
|
2026-05-26 03:13:06 +02:00
|
|
|
|
console.print(
|
|
|
|
|
|
Panel(
|
2026-06-22 01:39:07 +02:00
|
|
|
|
body,
|
2026-05-26 15:18:46 +02:00
|
|
|
|
title="Memory Activated (T03)",
|
2026-05-26 03:13:06 +02:00
|
|
|
|
border_style="blue",
|
|
|
|
|
|
padding=(0, 1),
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
2026-05-26 03:17:38 +02:00
|
|
|
|
# 2. Risk classification + mandatory confirmation (T03 safety; T04 memory signals)
|
|
|
|
|
|
assessment = classify(user_request, envelope, memory=memory)
|
2026-05-26 02:19:13 +02:00
|
|
|
|
|
|
|
|
|
|
if assessment.requires_confirmation:
|
|
|
|
|
|
from rich.table import Table
|
|
|
|
|
|
|
|
|
|
|
|
table = Table(
|
|
|
|
|
|
title=f"Risk Assessment — {assessment.level.value.upper()}",
|
|
|
|
|
|
show_header=False,
|
|
|
|
|
|
border_style="red",
|
|
|
|
|
|
)
|
|
|
|
|
|
table.add_row("Rationale", assessment.rationale)
|
|
|
|
|
|
if assessment.preview:
|
|
|
|
|
|
table.add_row("Preview", assessment.preview)
|
|
|
|
|
|
if assessment.affected_summary:
|
|
|
|
|
|
table.add_row("Would affect", assessment.affected_summary)
|
|
|
|
|
|
table.add_row("Rules", ", ".join(assessment.rules_triggered[:3]))
|
|
|
|
|
|
console.print(table)
|
|
|
|
|
|
|
|
|
|
|
|
if not get_user_confirmation(assessment):
|
|
|
|
|
|
console.print("[yellow]Action cancelled by user. No changes made.[/yellow]")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
if dry_run:
|
|
|
|
|
|
console.print("[green]--dry-run acknowledged.[/green] No side-effects.")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
2026-06-22 10:36:10 +02:00
|
|
|
|
# 3. Call through the single LLMAdapter boundary (T04 / CYA-WP-0008)
|
|
|
|
|
|
adapter = get_adapter(offline=offline)
|
2026-05-26 03:13:06 +02:00
|
|
|
|
ctx = (envelope.to_dict() if envelope else {}) or {}
|
|
|
|
|
|
ctx["memory"] = memory # T03: memory now in context passed to LLM (for personalization + explain)
|
2026-06-22 10:36:10 +02:00
|
|
|
|
if session_turns:
|
|
|
|
|
|
ctx["session_turns"] = bound_session_turns(session_turns)
|
2026-05-26 02:19:13 +02:00
|
|
|
|
llm_request = AssistanceRequest(
|
|
|
|
|
|
user_request=user_request,
|
2026-05-26 03:13:06 +02:00
|
|
|
|
context=ctx,
|
2026-05-26 02:19:13 +02:00
|
|
|
|
)
|
|
|
|
|
|
llm_response = adapter.complete(llm_request)
|
|
|
|
|
|
|
2026-05-26 03:13:06 +02:00
|
|
|
|
# 4. Render final user-facing artifact (T06 responsibility; T03 memory surface)
|
|
|
|
|
|
mem_line = ""
|
|
|
|
|
|
if memory.get("items"):
|
2026-05-26 15:18:46 +02:00
|
|
|
|
mem_line = f"\n[dim]Memory activated: {len(memory.get('items', []))} items (phase {memory.get('phase')})[/dim]"
|
2026-06-22 01:39:07 +02:00
|
|
|
|
refl_line = format_reflection_surfacing(memory, for_explain=False)
|
|
|
|
|
|
if refl_line:
|
|
|
|
|
|
mem_line += f"\n[cyan]{refl_line}[/cyan]"
|
2026-05-28 03:24:44 +02:00
|
|
|
|
|
2026-05-26 02:19:13 +02:00
|
|
|
|
console.print(
|
|
|
|
|
|
Panel(
|
|
|
|
|
|
f"[bold]Suggestion:[/bold]\n{llm_response.suggestion}\n\n"
|
|
|
|
|
|
f"[dim]{llm_response.explanation}\n"
|
2026-05-26 03:13:06 +02:00
|
|
|
|
f"Rationale: {llm_response.rationale}{mem_line}[/dim]",
|
2026-05-26 02:19:13 +02:00
|
|
|
|
title="LLM Response (via T04 seam)",
|
|
|
|
|
|
border_style="magenta",
|
|
|
|
|
|
padding=(1, 1),
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
console.print(
|
|
|
|
|
|
"[green]✓[/green] Request processed by orchestrator (T02+T03+T04 coordinated by T06)."
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-26 15:26:54 +02:00
|
|
|
|
def run_retrospection(scope: str = ".", limit: int = 8) -> None:
|
|
|
|
|
|
"""Guided retrospection session (T04 of CYA-WP-0003).
|
|
|
|
|
|
|
|
|
|
|
|
Helps the user review recent memory usage in the given scope,
|
|
|
|
|
|
reflect, and record new interaction goals or preferences.
|
|
|
|
|
|
These are stored using the retrospection-aware memory helper.
|
|
|
|
|
|
"""
|
|
|
|
|
|
console.print(
|
|
|
|
|
|
Panel(
|
|
|
|
|
|
"[bold cyan]Retrospection Session[/bold cyan]\n\n"
|
|
|
|
|
|
f"Scope: [green]{scope}[/green]\n"
|
|
|
|
|
|
"We will look at recent memory items and help you reflect.\n"
|
|
|
|
|
|
"Your answers will be stored as durable retrospection memory.",
|
|
|
|
|
|
title="cya retrospect",
|
|
|
|
|
|
border_style="magenta",
|
|
|
|
|
|
padding=(1, 2),
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Recall recent items, with bias toward retrospection kinds if present
|
|
|
|
|
|
try:
|
|
|
|
|
|
recent = recall_preferences(
|
|
|
|
|
|
scope,
|
|
|
|
|
|
limit=limit,
|
|
|
|
|
|
kinds=[KIND_RETROSPECTION, KIND_INTERACTION_GOAL, "preference"],
|
|
|
|
|
|
)
|
|
|
|
|
|
items = recent.get("items", [])
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
console.print(f"[red]Could not load memory: {e}[/red]")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
if not items:
|
|
|
|
|
|
console.print(
|
|
|
|
|
|
"[yellow]No memory items found in this scope yet.[/yellow]\n"
|
|
|
|
|
|
"You can create some with normal usage or explicit remembers."
|
|
|
|
|
|
)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
console.print(
|
|
|
|
|
|
Panel(
|
|
|
|
|
|
"\n".join(
|
|
|
|
|
|
f"• [bold]{item.get('key')}[/bold]: {item.get('value')}"
|
|
|
|
|
|
for item in items[:5]
|
|
|
|
|
|
),
|
|
|
|
|
|
title=f"Recent Memory in {scope} (showing up to 5)",
|
|
|
|
|
|
border_style="blue",
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Simple guided reflection
|
|
|
|
|
|
console.print("\n[bold]Reflection time[/bold]")
|
|
|
|
|
|
|
|
|
|
|
|
what_worked = typer.prompt(
|
|
|
|
|
|
"What worked well in recent assistance? (short answer or 'skip')",
|
|
|
|
|
|
default="",
|
|
|
|
|
|
show_default=False,
|
|
|
|
|
|
)
|
|
|
|
|
|
if what_worked and what_worked.lower() not in ("skip", "s", ""):
|
|
|
|
|
|
remember_retrospection_outcome(
|
|
|
|
|
|
"what_worked", what_worked, scope=scope
|
|
|
|
|
|
)
|
|
|
|
|
|
console.print("[green]Recorded.[/green]")
|
|
|
|
|
|
|
|
|
|
|
|
what_to_change = typer.prompt(
|
|
|
|
|
|
"What should change in future interactions? (e.g. 'be more concise', 'always show alternatives')",
|
|
|
|
|
|
default="",
|
|
|
|
|
|
show_default=False,
|
|
|
|
|
|
)
|
|
|
|
|
|
if what_to_change and what_to_change.lower() not in ("skip", "s", ""):
|
|
|
|
|
|
remember_retrospection_outcome(
|
|
|
|
|
|
"interaction_goal", what_to_change, scope=scope
|
|
|
|
|
|
)
|
|
|
|
|
|
console.print("[green]Recorded as interaction goal.[/green]")
|
|
|
|
|
|
|
|
|
|
|
|
safety_note = typer.prompt(
|
|
|
|
|
|
"Any standing safety or preference rules for this project? (optional)",
|
|
|
|
|
|
default="",
|
|
|
|
|
|
show_default=False,
|
|
|
|
|
|
)
|
|
|
|
|
|
if safety_note and safety_note.lower() not in ("skip", "s", ""):
|
|
|
|
|
|
remember_retrospection_outcome(
|
|
|
|
|
|
"safety_preference", safety_note, scope=scope
|
|
|
|
|
|
)
|
|
|
|
|
|
console.print("[green]Recorded as safety preference.[/green]")
|
|
|
|
|
|
|
2026-06-22 01:39:07 +02:00
|
|
|
|
_capture_reflection_lessons(scope)
|
|
|
|
|
|
_offer_reflection_compaction(scope)
|
2026-05-28 03:24:44 +02:00
|
|
|
|
|
2026-05-26 15:26:54 +02:00
|
|
|
|
console.print(
|
|
|
|
|
|
Panel(
|
|
|
|
|
|
"Thank you. Your reflections have been stored as retrospection memory.\n"
|
|
|
|
|
|
"They will be preferentially activated in future sessions in this scope.\n\n"
|
|
|
|
|
|
"You can review them anytime with:\n"
|
2026-06-22 01:39:07 +02:00
|
|
|
|
f" [bold]cya memory reflections --scope {scope}[/bold]\n"
|
2026-05-26 15:26:54 +02:00
|
|
|
|
f" [bold]cya --explain-context \"...\"[/bold] (in this directory)\n"
|
|
|
|
|
|
f" or inspect the JSON files in [cyan]~/.config/cya/memory/[/cyan]",
|
|
|
|
|
|
title="Retrospection Complete",
|
|
|
|
|
|
border_style="green",
|
|
|
|
|
|
padding=(1, 2),
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-22 01:39:07 +02:00
|
|
|
|
def _capture_reflection_lessons(scope: str) -> None:
|
|
|
|
|
|
"""Profile 1: guided verbal lesson capture with preview and confirmation."""
|
|
|
|
|
|
want = typer.prompt(
|
|
|
|
|
|
"Capture 1–3 verbal lessons from this session? (y/n)",
|
|
|
|
|
|
default="n",
|
|
|
|
|
|
show_default=False,
|
|
|
|
|
|
)
|
|
|
|
|
|
if not want or want.strip().lower() in ("n", "no", "skip", "s", ""):
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
answers: dict[str, str] = {}
|
|
|
|
|
|
for prompt_key, label in REFLECTION_CAPTURE_PROMPTS:
|
|
|
|
|
|
answers[prompt_key] = typer.prompt(label, default="", show_default=False)
|
|
|
|
|
|
|
|
|
|
|
|
lessons = collect_lessons_from_answers(answers)
|
|
|
|
|
|
if not lessons:
|
|
|
|
|
|
console.print("[yellow]No lessons captured — nothing stored.[/yellow]")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
console.print(
|
|
|
|
|
|
Panel(
|
|
|
|
|
|
preview_lessons(lessons),
|
|
|
|
|
|
title="Preview — verbal lessons to save",
|
|
|
|
|
|
border_style="cyan",
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
confirm = typer.prompt("Save these lessons? (y/n)", default="n", show_default=False)
|
|
|
|
|
|
if confirm.strip().lower() not in ("y", "yes"):
|
|
|
|
|
|
console.print("[yellow]Lessons discarded — nothing stored.[/yellow]")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
count = save_reflection_lessons(
|
|
|
|
|
|
lessons,
|
|
|
|
|
|
scope,
|
|
|
|
|
|
provenance=session_provenance(scope),
|
|
|
|
|
|
)
|
|
|
|
|
|
console.print(f"[green]Saved {count} verbal reflection(s) (Profile 1).[/green]")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _offer_reflection_compaction(scope: str) -> None:
|
|
|
|
|
|
"""Offer explicit merge of near-duplicate reflections in this scope."""
|
|
|
|
|
|
groups = find_duplicate_reflection_groups(scope)
|
|
|
|
|
|
if not groups:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
console.print(
|
|
|
|
|
|
Panel(
|
|
|
|
|
|
f"Found {len(groups)} group(s) of similar reflections in scope [green]{scope}[/green].\n"
|
|
|
|
|
|
"Compaction is opt-in — nothing is deleted without your confirmation.",
|
|
|
|
|
|
title="Reflection compaction available",
|
|
|
|
|
|
border_style="yellow",
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
do_compact = typer.prompt("Review and compact duplicates? (y/n)", default="n", show_default=False)
|
|
|
|
|
|
if do_compact.strip().lower() not in ("y", "yes"):
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
for group in groups:
|
|
|
|
|
|
console.print("\n[bold]Similar reflections:[/bold]")
|
|
|
|
|
|
for item in group:
|
|
|
|
|
|
console.print(f" • {item.get('key')}: {item.get('value')}")
|
|
|
|
|
|
|
|
|
|
|
|
keep = typer.prompt(
|
|
|
|
|
|
"Key to keep (or 'skip' this group)",
|
|
|
|
|
|
default=group[0].get("key", ""),
|
|
|
|
|
|
show_default=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
if not keep or keep.strip().lower() in ("skip", "s"):
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
remove_keys = [i.get("key") for i in group if i.get("key") != keep]
|
|
|
|
|
|
merge = typer.prompt(
|
|
|
|
|
|
"Merged text (Enter to keep existing value of kept key)",
|
|
|
|
|
|
default="",
|
|
|
|
|
|
show_default=False,
|
|
|
|
|
|
)
|
|
|
|
|
|
result = compact_reflections(
|
|
|
|
|
|
scope,
|
|
|
|
|
|
keep_key=keep,
|
|
|
|
|
|
remove_keys=[k for k in remove_keys if k],
|
|
|
|
|
|
merged_value=merge.strip() or None,
|
|
|
|
|
|
)
|
|
|
|
|
|
console.print(
|
|
|
|
|
|
f"[green]Compacted: kept {result['kept']}, removed {len(result['removed'])}[/green]"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-26 15:26:54 +02:00
|
|
|
|
__all__ = ["handle_request", "run_retrospection"]
|