File size: 5,687 Bytes
039dd77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
"""ayllu.loop — the a11oy-native turn, learned from the tribe's shared brain loop.

The tribe ran every soul through one tool-calling brain. a11oy runs every persona
through a11oy's own machinery:
  * model tier -> a11oy_active_flux_router.router_crossover  (ADVISORY pre-estimate)
  * model call -> injected model_complete (see ayllu.backend), which delegates to
                  a11oy's orchestrator and does the REAL routing + energy receipts
Tool dispatch (the gated a11oy_agent_loop.AgentLoop + ayllu.autonomy.gate) is a
separate, clearly-labeled follow-up and is NOT claimed as active here. This module
never fabricates an answer and never claims a wiring it doesn't have.
"""
from __future__ import annotations

from typing import Any, Awaitable, Callable, Optional


def select_tier(query_difficulty: float = 0.5) -> dict[str, Any]:
    """ADVISORY tier pre-estimate via a11oy's active-flux router.

    Advisory only: the ACTUAL model is chosen by the backend's own router
    (a11oy_code_orchestrator.route). Honest deterministic fallback (threshold 0.5)
    when the router is absent.
    """
    try:
        import a11oy_active_flux_router as _afr  # type: ignore

        cross = _afr.router_crossover(query_difficulty=float(query_difficulty))
        return {
            "route": cross.get("route"),
            "regime": cross.get("regime"),
            "role": "advisory",
            "source": "a11oy_active_flux_router",
            "detail": cross,
        }
    except Exception as exc:  # honest fallback — router not importable here
        route = "small/local" if float(query_difficulty) < 0.5 else "large/cloud"
        return {
            "route": route,
            "regime": "easy" if route == "small/local" else "hard",
            "role": "advisory",
            "source": "honest-fallback",
            "note": f"active-flux router unavailable ({str(exc)[:80]}); "
                    "deterministic 0.5 threshold used",
        }


async def run_turn(
    persona,
    prompt: str,
    *,
    model_complete: Optional[Callable[..., Awaitable[Any]]] = None,
    execute_tool: Optional[Callable[..., Awaitable[dict]]] = None,   # reserved (follow-up)
    khipu_emit: Optional[Callable[[str, dict], dict]] = None,        # reserved (follow-up)
    puriq_decide: Optional[Callable[[str, dict], dict]] = None,      # reserved (follow-up)
    difficulty: Optional[float] = None,
    two_person_attested: bool = False,
) -> dict[str, Any]:
    """Run one persona's turn.

    With a `model_complete` backend injected, this performs a DIRECT model completion
    (no tool dispatch, no state change) and reports the ACTUAL model the backend used.
    Without a backend, it is HONEST: persona + advisory tier + posture only, no answer.
    It NEVER fabricates a reply, and it does NOT claim the bounded tool-loop it isn't
    running.
    """
    diff = persona.default_difficulty if difficulty is None else float(difficulty)
    tier = select_tier(diff)
    system = persona.system_prompt()

    answer: Optional[str] = None
    model: Optional[str] = None
    stub: Optional[bool] = None
    energy_receipt: Any = None

    if model_complete is None:
        honesty = ("model backend not injected — no answer fabricated. This turn "
                   "returns the persona, the advisory model tier, and the bounded-"
                   "autonomy posture only.")
        loop_info = {
            "mode": "no-backend",
            "tool_dispatch": False,
            "note": "ayllu.autonomy.gate is AVAILABLE for any future tool dispatch but "
                    "is not invoked here — this turn runs no tools and changes no state",
        }
    else:
        # DIRECT completion. We deliberately do NOT construct a11oy_agent_loop.AgentLoop:
        # this turn dispatches no tools and changes no state, so claiming the bounded
        # tool-loop would be an overclaim. The gated AgentLoop + ayllu.autonomy.gate are
        # reserved for the tool-calling follow-up.
        loop_info = {
            "mode": "direct-completion",
            "tool_dispatch": False,
            "note": "direct model completion via a11oy's orchestrator; no tools, no state "
                    "change; a11oy_agent_loop.AgentLoop + ayllu.autonomy.gate reserved for "
                    "the tool-calling follow-up and NOT claimed as active here",
        }
        try:
            result = await model_complete(system=system, prompt=prompt,
                                          tier=tier.get("route"), persona=persona.name)
            if isinstance(result, dict):
                answer = result.get("text")
                model = result.get("model")
                stub = result.get("stub")
                energy_receipt = result.get("energy_receipt")
            else:
                answer = str(result)
            honesty = "answer produced by a11oy's model backend" + (
                " (clearly-labeled stub — no inference credential set)" if stub else "")
        except Exception as exc:
            honesty = (f"model backend raised: {str(exc)[:120]} "
                       "(honest — no fabricated answer)")

    return {
        "persona": persona.name,
        "quechua": persona.quechua,
        "archetype": persona.archetype,
        "domain": persona.domain,
        "tier": tier,
        "tier_note": "advisory pre-estimate; the ACTUAL model was selected by a11oy's "
                     "orchestrator router — see 'model'",
        "loop": loop_info,
        "answer": answer,
        "model": model,
        "stub": stub,
        "energy_receipt": energy_receipt,
        "honesty": honesty,
        "evidence": [],
    }