otto-cli-agent 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 (129) hide show
  1. agent/README.md +22 -0
  2. agent/__init__.py +0 -0
  3. agent/cli/README.md +77 -0
  4. agent/cli/__init__.py +0 -0
  5. agent/cli/art.py +371 -0
  6. agent/cli/chat.py +309 -0
  7. agent/cli/clipboard.py +106 -0
  8. agent/cli/context.py +56 -0
  9. agent/cli/doctor.py +60 -0
  10. agent/cli/errors.py +58 -0
  11. agent/cli/eval.py +130 -0
  12. agent/cli/eval_claw.py +414 -0
  13. agent/cli/eval_compaction.py +106 -0
  14. agent/cli/eval_hle.py +92 -0
  15. agent/cli/eval_memory.py +253 -0
  16. agent/cli/eval_swe.py +172 -0
  17. agent/cli/lessons.py +97 -0
  18. agent/cli/main.py +67 -0
  19. agent/cli/modals.py +570 -0
  20. agent/cli/models.py +64 -0
  21. agent/cli/output.py +54 -0
  22. agent/cli/route.py +78 -0
  23. agent/cli/sessions.py +125 -0
  24. agent/cli/setup_screen.py +562 -0
  25. agent/cli/shell.py +548 -0
  26. agent/cli/tui.py +1807 -0
  27. agent/cli/ui.py +14 -0
  28. agent/cli/usage_panel.py +159 -0
  29. agent/config/README.md +7 -0
  30. agent/config/__init__.py +0 -0
  31. agent/config/envfile.py +76 -0
  32. agent/eval/README.md +76 -0
  33. agent/eval/__init__.py +0 -0
  34. agent/eval/claw_bench.py +1031 -0
  35. agent/eval/compaction_bench.py +229 -0
  36. agent/eval/data/README.md +10 -0
  37. agent/eval/data/claw/README.md +108 -0
  38. agent/eval/data/claw/llm_judge-gemini.patch +57 -0
  39. agent/eval/data/claw/otto.yaml +35 -0
  40. agent/eval/failures.py +276 -0
  41. agent/eval/golden/README.md +33 -0
  42. agent/eval/golden/code_01.json +6 -0
  43. agent/eval/golden/code_02.json +6 -0
  44. agent/eval/golden/code_03.json +6 -0
  45. agent/eval/golden/code_04.json +6 -0
  46. agent/eval/golden/code_05.json +6 -0
  47. agent/eval/golden/code_06.json +6 -0
  48. agent/eval/golden/math_01.json +6 -0
  49. agent/eval/golden/math_02.json +6 -0
  50. agent/eval/golden/math_03.json +6 -0
  51. agent/eval/golden/math_04.json +6 -0
  52. agent/eval/golden/math_05.json +6 -0
  53. agent/eval/golden/math_06.json +6 -0
  54. agent/eval/golden/nphard_gcp_01.json +6 -0
  55. agent/eval/golden/nphard_ksp_01.json +6 -0
  56. agent/eval/golden/nphard_math_binpacking_01.json +6 -0
  57. agent/eval/golden/nphard_math_clique_01.json +6 -0
  58. agent/eval/golden/nphard_math_setcover_01.json +6 -0
  59. agent/eval/golden/nphard_math_subsetsum_01.json +6 -0
  60. agent/eval/golden/nphard_tsp_01.json +6 -0
  61. agent/eval/golden/nphard_tsp_02.json +6 -0
  62. agent/eval/hle_bench.py +273 -0
  63. agent/eval/langfuse_sync.py +172 -0
  64. agent/eval/memory_bench.py +538 -0
  65. agent/eval/runner.py +174 -0
  66. agent/eval/single_agent.py +120 -0
  67. agent/eval/swe_bench.py +604 -0
  68. agent/eval/terminal_bench.py +345 -0
  69. agent/memory/README.md +102 -0
  70. agent/memory/__init__.py +42 -0
  71. agent/memory/embeddings.py +302 -0
  72. agent/memory/hashing.py +15 -0
  73. agent/memory/lessons.py +483 -0
  74. agent/memory/queue.py +531 -0
  75. agent/memory/retrieval.py +493 -0
  76. agent/memory/session.py +60 -0
  77. agent/memory/sessions.py +436 -0
  78. agent/memory/store.py +429 -0
  79. agent/memory/tokens.py +60 -0
  80. agent/memory/wiring.py +146 -0
  81. agent/pipeline/README.md +135 -0
  82. agent/pipeline/__init__.py +0 -0
  83. agent/pipeline/browsing.py +609 -0
  84. agent/pipeline/budget.py +403 -0
  85. agent/pipeline/codemap.py +254 -0
  86. agent/pipeline/evidence.py +325 -0
  87. agent/pipeline/execution.py +67 -0
  88. agent/pipeline/modes.py +137 -0
  89. agent/pipeline/native.py +1137 -0
  90. agent/pipeline/nodes.py +3644 -0
  91. agent/pipeline/pricing.py +209 -0
  92. agent/pipeline/progress.py +139 -0
  93. agent/pipeline/rag.py +139 -0
  94. agent/pipeline/research.py +1325 -0
  95. agent/pipeline/run.py +528 -0
  96. agent/pipeline/screen.py +77 -0
  97. agent/pipeline/state.py +220 -0
  98. agent/pipeline/toolkit.py +328 -0
  99. agent/pipeline/tools.py +1990 -0
  100. agent/pipeline/tracing.py +147 -0
  101. agent/pipeline/usage.py +251 -0
  102. agent/pipeline/vision.py +84 -0
  103. agent/pipeline/walkthrough.py +735 -0
  104. agent/pipeline/workspace.py +229 -0
  105. agent/router/README.md +60 -0
  106. agent/router/__init__.py +0 -0
  107. agent/router/automap.py +114 -0
  108. agent/router/health.py +229 -0
  109. agent/router/llm_provider/README.md +38 -0
  110. agent/router/llm_provider/__init__.py +202 -0
  111. agent/router/llm_provider/anthropic_provider.py +128 -0
  112. agent/router/llm_provider/base.py +507 -0
  113. agent/router/llm_provider/custom.py +152 -0
  114. agent/router/llm_provider/gemini_provider.py +122 -0
  115. agent/router/llm_provider/inception_provider.py +687 -0
  116. agent/router/llm_provider/openai_provider.py +151 -0
  117. agent/router/llm_provider/retired.py +145 -0
  118. agent/router/llm_provider/temperature.py +371 -0
  119. agent/router/mapping.py +579 -0
  120. agent/router/outcomes.py +363 -0
  121. agent/router/overrides.py +389 -0
  122. agent/router/reload.py +28 -0
  123. agent/router/router.py +413 -0
  124. agent/router/setup.py +123 -0
  125. otto_cli_agent-0.1.0.dist-info/METADATA +115 -0
  126. otto_cli_agent-0.1.0.dist-info/RECORD +129 -0
  127. otto_cli_agent-0.1.0.dist-info/WHEEL +4 -0
  128. otto_cli_agent-0.1.0.dist-info/entry_points.txt +2 -0
  129. otto_cli_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,389 @@
1
+ """What a person has pinned on top of the shipped routing table, and where.
2
+
3
+ `agent/router/mapping.py`'s TASK_ROUTES is a measured, hand-tuned table in
4
+ source. It cannot know which keys THIS machine has or which model its owner
5
+ wants judging their work, and editing Python to say so is not a setting. So
6
+ this module keeps a small file beside the other per-installation state:
7
+
8
+ ~/.otto/routes.json
9
+ {
10
+ "version": 1,
11
+ "pins": {"reason": "openai:gpt-5-mini", "chat_fast": "ollama:llama3.2:latest"},
12
+ "endpoints": {"ollama": {"label": "Ollama",
13
+ "capabilities": {"llama3.2:latest": ["chat", "tools"]}}}
14
+ }
15
+
16
+ `apply()` turns that into the live table, IN PLACE: each custom endpoint is
17
+ registered as a provider (agent/router/llm_provider/custom.py) and each pin
18
+ becomes a Candidate at the HEAD of that task's chain, with the shipped chain
19
+ kept behind it as the fallback path. In place, because two Routers read
20
+ TASK_ROUTES at resolve time -- the pipeline's module-level one and the CLI's
21
+ -- and a new dict would reach neither.
22
+
23
+ What a pin does not do: it does not bypass the endpoint. A pin on VISION still
24
+ requires the model to see, a pin on CODE_COMPLETE still has to be an Inception
25
+ FIM model, and WEB stays Anthropic because the web_search tool binds
26
+ Anthropic's own server-side search (agent/pipeline/tools.py). `validate_pin`
27
+ says so up front rather than letting a pin resolve cleanly and fail mid-run.
28
+
29
+ Keys are NOT here. They stay in the repo `.env` (agent/config/envfile.py),
30
+ which the four built-in vendors already read; a custom endpoint's
31
+ `<NAME>_API_KEY` and `<NAME>_BASE_URL` rows go beside them. This file holds
32
+ names and choices, nothing secret, and can be copied between machines as is.
33
+ """
34
+ from __future__ import annotations
35
+
36
+ import json
37
+ import logging
38
+ import os
39
+ from contextlib import contextmanager
40
+ from contextvars import ContextVar
41
+ from dataclasses import dataclass, field
42
+ from pathlib import Path
43
+ from typing import Iterable, Iterator, Mapping
44
+
45
+ from agent.router import mapping
46
+ from agent.router.llm_provider import custom, is_custom, register_custom, unregister_custom
47
+ from agent.router.llm_provider import temperature
48
+ from agent.router.llm_provider.base import Capability
49
+ from agent.router.mapping import (
50
+ ENDPOINT_CAPABILITY,
51
+ INCEPTION_ONLY_ENDPOINTS,
52
+ PARAMS_BY_PROVIDER,
53
+ TASK_ROUTES,
54
+ Candidate,
55
+ MappingError,
56
+ Task,
57
+ )
58
+ from agent.router.outcomes import DB_DIR
59
+
60
+ log = logging.getLogger(__name__)
61
+
62
+ VERSION = 1
63
+ #: Set to skip the file entirely -- an eval harness that must measure the
64
+ #: shipped table, not this machine's preferences.
65
+ IGNORE_ENV = "OTTO_IGNORE_ROUTES"
66
+ #: Point at a different file (tests; a second profile).
67
+ PATH_ENV = "OTTO_ROUTES"
68
+
69
+ #: Which provider a task is bound to regardless of pins, and why.
70
+ PROVIDER_ONLY: dict[Task, tuple[str, str]] = {
71
+ Task.WEB: ("anthropic", "web_search binds Anthropic's server-side search tool"),
72
+ Task.CODE_COMPLETE: ("inception", "only Inception serves the FIM endpoint"),
73
+ Task.CODE_EDIT: ("inception", "only Inception serves the edit endpoint"),
74
+ }
75
+
76
+
77
+ class PinError(ValueError):
78
+ """A pin that can be seen to be wrong without asking a provider."""
79
+
80
+
81
+ @dataclass(frozen=True)
82
+ class EndpointSpec:
83
+ label: str = ""
84
+ #: model id -> capability names, overriding the heuristic classifier.
85
+ capabilities: dict[str, tuple[str, ...]] = field(default_factory=dict)
86
+
87
+
88
+ @dataclass(frozen=True)
89
+ class Overrides:
90
+ pins: dict[Task, str] = field(default_factory=dict)
91
+ endpoints: dict[str, EndpointSpec] = field(default_factory=dict)
92
+ version: int = VERSION
93
+
94
+
95
+ # --------------------------------------------------------------------------
96
+ # Where the file is
97
+ # --------------------------------------------------------------------------
98
+
99
+ _UNSET = object()
100
+ _PATH: ContextVar = ContextVar("otto_routes_path", default=_UNSET)
101
+
102
+
103
+ def routes_path() -> Path:
104
+ bound = _PATH.get()
105
+ if bound is not _UNSET:
106
+ return Path(bound)
107
+ env = os.environ.get(PATH_ENV, "").strip()
108
+ return Path(env).expanduser() if env else DB_DIR / "routes.json"
109
+
110
+
111
+ @contextmanager
112
+ def bind_routes(path: Path | str) -> Iterator[Path]:
113
+ """Use `path` for the duration -- a test's tmp_path. Same seam shape as
114
+ agent/router/outcomes.py's bind_log."""
115
+ token = _PATH.set(Path(path))
116
+ try:
117
+ yield Path(path)
118
+ finally:
119
+ _PATH.reset(token)
120
+
121
+
122
+ # --------------------------------------------------------------------------
123
+ # Load / save
124
+ # --------------------------------------------------------------------------
125
+
126
+ def load(path: Path | None = None) -> Overrides:
127
+ """The file's content, or an empty Overrides. Never raises: a corrupt
128
+ preferences file must not stop otto from starting -- it is logged and
129
+ the shipped table is used."""
130
+ path = path or routes_path()
131
+ try:
132
+ raw = json.loads(path.read_text())
133
+ except FileNotFoundError:
134
+ return Overrides()
135
+ except (OSError, ValueError) as exc:
136
+ log.warning("ignoring %s: %s", path, exc)
137
+ return Overrides()
138
+ if not isinstance(raw, dict) or raw.get("version") != VERSION:
139
+ log.warning("ignoring %s: unsupported format", path)
140
+ return Overrides()
141
+
142
+ pins: dict[Task, str] = {}
143
+ for key, spec in (raw.get("pins") or {}).items():
144
+ try:
145
+ task = Task(key)
146
+ except ValueError:
147
+ log.warning("%s: unknown task %r, skipped", path, key)
148
+ continue
149
+ if isinstance(spec, str) and spec.strip():
150
+ pins[task] = spec.strip()
151
+
152
+ endpoints: dict[str, EndpointSpec] = {}
153
+ for name, body in (raw.get("endpoints") or {}).items():
154
+ try:
155
+ custom.validate_name(name)
156
+ except ValueError as exc:
157
+ log.warning("%s: %s, skipped", path, exc)
158
+ continue
159
+ body = body if isinstance(body, dict) else {}
160
+ caps = {
161
+ str(model_id): tuple(str(c) for c in values)
162
+ for model_id, values in (body.get("capabilities") or {}).items()
163
+ if isinstance(values, (list, tuple))
164
+ }
165
+ endpoints[name] = EndpointSpec(label=str(body.get("label") or ""), capabilities=caps)
166
+ return Overrides(pins=pins, endpoints=endpoints)
167
+
168
+
169
+ def save(o: Overrides, path: Path | None = None) -> None:
170
+ path = path or routes_path()
171
+ payload = {
172
+ "version": VERSION,
173
+ "pins": {task.value: spec for task, spec in sorted(o.pins.items(), key=lambda kv: kv[0].value)},
174
+ "endpoints": {
175
+ name: {"label": spec.label,
176
+ **({"capabilities": {m: list(c) for m, c in spec.capabilities.items()}}
177
+ if spec.capabilities else {})}
178
+ for name, spec in sorted(o.endpoints.items())
179
+ },
180
+ }
181
+ path.parent.mkdir(parents=True, exist_ok=True)
182
+ tmp = path.with_suffix(path.suffix + ".tmp")
183
+ tmp.write_text(json.dumps(payload, indent=2) + "\n")
184
+ os.replace(tmp, path)
185
+
186
+
187
+ # --------------------------------------------------------------------------
188
+ # Applying to the live table
189
+ # --------------------------------------------------------------------------
190
+
191
+ #: The table as shipped -- snapshotted once, before any pin touches it, so a
192
+ #: cleared pin restores exactly what mapping.py declared.
193
+ _SHIPPED: dict[Task, tuple[Candidate, ...]] = dict(TASK_ROUTES)
194
+ #: The pin Candidate at the head of each pinned task, by identity. What
195
+ #: `is_pin()` answers from -- the router asks it to skip evidence reordering.
196
+ _ACTIVE: dict[Task, Candidate] = {}
197
+ _REGISTERED: set[str] = set()
198
+ _PROBLEMS: list[str] = []
199
+
200
+
201
+ def shipped(task: Task) -> tuple[Candidate, ...]:
202
+ return _SHIPPED[task]
203
+
204
+
205
+ def is_pin(task: Task, c: Candidate) -> bool:
206
+ return _ACTIVE.get(task) is c
207
+
208
+
209
+ def pinned_spec(task: Task) -> str | None:
210
+ pin = _ACTIVE.get(task)
211
+ return pin.spec if pin is not None else None
212
+
213
+
214
+ def active_pins() -> dict[Task, str]:
215
+ return {task: c.spec for task, c in _ACTIVE.items() if c.spec}
216
+
217
+
218
+ def last_problems() -> list[str]:
219
+ return list(_PROBLEMS)
220
+
221
+
222
+ def validate_pin(task: Task, spec: str) -> None:
223
+ """Shape and policy only -- whether the model exists is a question for a
224
+ live catalogue, which the setup screen asks before offering it."""
225
+ provider, sep, model = (spec or "").partition(":")
226
+ if not sep or not provider or not model:
227
+ raise PinError(f"{spec!r} is not 'provider:model'")
228
+ if provider not in mapping.known_providers():
229
+ raise PinError(
230
+ f"unknown provider {provider!r}; known: {sorted(mapping.known_providers())}")
231
+ bound = PROVIDER_ONLY.get(task)
232
+ if bound and provider != bound[0]:
233
+ raise PinError(f"{task.value} must stay on {bound[0]}: {bound[1]}")
234
+
235
+
236
+ def _pin_requires(head: Candidate) -> frozenset[Capability]:
237
+ # The endpoint's own capability always, VISION always. Nothing else: a
238
+ # person's explicit choice is not gated by a heuristic REASONING tag.
239
+ return frozenset({ENDPOINT_CAPABILITY[head.endpoint]}) | (head.requires & {Capability.VISION})
240
+
241
+
242
+ def _pin_params(provider: str, chain: tuple[Candidate, ...]) -> dict:
243
+ head = chain[0]
244
+ for c in chain:
245
+ if c.provider_name == provider:
246
+ return dict(c.params)
247
+ allowed = PARAMS_BY_PROVIDER.get(provider, {}).get(head.endpoint)
248
+ if allowed is not None:
249
+ return {k: v for k, v in head.params.items() if k in allowed}
250
+ return {k: v for k, v in head.params.items() if k in ("temperature", "max_tokens")}
251
+
252
+
253
+ def _register_endpoints(endpoints: Mapping[str, EndpointSpec]) -> list[str]:
254
+ problems: list[str] = []
255
+ for name in list(_REGISTERED - set(endpoints)):
256
+ unregister_custom(name)
257
+ mapping.unregister_provider_name(name)
258
+ _REGISTERED.discard(name)
259
+ for name, spec in endpoints.items():
260
+ try:
261
+ cls = custom.openai_compatible(name, capability_overrides=spec.capabilities)
262
+ register_custom(name, cls)
263
+ except Exception as exc: # bad name, SDK missing
264
+ problems.append(f"endpoint {name!r} not registered: {exc}")
265
+ continue
266
+ mapping.register_provider_name(name, params_like="openai")
267
+ temperature.register_provider(name)
268
+ _REGISTERED.add(name)
269
+ return problems
270
+
271
+
272
+ def apply(routes: dict[Task, tuple[Candidate, ...]] = TASK_ROUTES,
273
+ o: Overrides | None = None, *,
274
+ shipped_routes: Mapping[Task, tuple[Candidate, ...]] | None = None) -> list[str]:
275
+ """Rebuild `routes` from the shipped table plus `o`. Idempotent. Returns
276
+ the problems it found; every task with a problem keeps its shipped chain.
277
+
278
+ `shipped_routes` is for a caller working on a copy of the table (tests);
279
+ the live table always restores from the snapshot taken at import.
280
+ """
281
+ o = load() if o is None else o
282
+ base = dict(shipped_routes) if shipped_routes is not None else (
283
+ _SHIPPED if routes is TASK_ROUTES else dict(routes))
284
+ problems = _register_endpoints(o.endpoints)
285
+
286
+ for task, chain in base.items():
287
+ routes[task] = chain
288
+ _ACTIVE.clear()
289
+
290
+ for task, spec in o.pins.items():
291
+ chain = base[task]
292
+ try:
293
+ validate_pin(task, spec)
294
+ provider, _, _ = spec.partition(":")
295
+ pin = Candidate(
296
+ spec=spec,
297
+ requires=_pin_requires(chain[0]),
298
+ endpoint=chain[0].endpoint,
299
+ params=_pin_params(provider, chain),
300
+ )
301
+ new_chain = (pin, *[c for c in chain if c.spec != spec])
302
+ mapping.validate({**routes, task: new_chain})
303
+ except (PinError, MappingError) as exc:
304
+ problems.append(f"pin {task.value} -> {spec}: {exc}")
305
+ continue
306
+ routes[task] = new_chain
307
+ _ACTIVE[task] = pin
308
+
309
+ _PROBLEMS[:] = problems
310
+ for problem in problems:
311
+ log.warning("routes: %s", problem)
312
+ return problems
313
+
314
+
315
+ def apply_at_startup() -> None:
316
+ """Called by agent/cli/main.py before the pipeline imports. Swallows
317
+ everything: a preferences file can degrade routing, never prevent it."""
318
+ if os.environ.get(IGNORE_ENV, "").strip():
319
+ return
320
+ try:
321
+ apply()
322
+ except Exception as exc: # pragma: no cover -- last line of defence
323
+ log.warning("could not apply %s: %s", routes_path(), exc)
324
+
325
+
326
+ # --------------------------------------------------------------------------
327
+ # Edits -- each saves the file and re-applies
328
+ # --------------------------------------------------------------------------
329
+
330
+ def pins() -> dict[Task, str]:
331
+ return dict(load().pins)
332
+
333
+
334
+ def endpoints() -> dict[str, EndpointSpec]:
335
+ return dict(load().endpoints)
336
+
337
+
338
+ def set_pin(task: Task, spec: str) -> None:
339
+ o = load()
340
+ if o.endpoints:
341
+ _register_endpoints(o.endpoints) # so validate_pin knows custom names
342
+ validate_pin(task, spec)
343
+ new = Overrides(pins={**o.pins, task: spec}, endpoints=o.endpoints)
344
+ problems = [p for p in apply(o=new) if p.startswith(f"pin {task.value} ")]
345
+ if problems:
346
+ apply(o=o)
347
+ raise PinError(problems[0])
348
+ save(new)
349
+
350
+
351
+ def clear_pin(task: Task) -> None:
352
+ o = load()
353
+ if task not in o.pins:
354
+ return
355
+ new = Overrides(pins={t: s for t, s in o.pins.items() if t != task}, endpoints=o.endpoints)
356
+ save(new)
357
+ apply(o=new)
358
+
359
+
360
+ def add_endpoint(name: str, label: str = "") -> None:
361
+ custom.validate_name(name)
362
+ o = load()
363
+ spec = o.endpoints.get(name, EndpointSpec())
364
+ new = Overrides(pins=o.pins, endpoints={**o.endpoints, name: EndpointSpec(label or spec.label, spec.capabilities)})
365
+ save(new)
366
+ apply(o=new)
367
+
368
+
369
+ def remove_endpoint(name: str) -> None:
370
+ """Forget an endpoint and every pin on it. The caller unsets the .env rows."""
371
+ o = load()
372
+ new = Overrides(
373
+ pins={t: s for t, s in o.pins.items() if s.partition(":")[0] != name},
374
+ endpoints={n: e for n, e in o.endpoints.items() if n != name},
375
+ )
376
+ save(new)
377
+ apply(o=new)
378
+
379
+
380
+ def set_capabilities(endpoint: str, model_id: str, caps: Iterable[Capability | str]) -> None:
381
+ o = load()
382
+ if endpoint not in o.endpoints:
383
+ raise PinError(f"no endpoint called {endpoint!r}")
384
+ spec = o.endpoints[endpoint]
385
+ names = tuple(Capability(c).value for c in caps)
386
+ new_spec = EndpointSpec(spec.label, {**spec.capabilities, model_id: names})
387
+ new = Overrides(pins=o.pins, endpoints={**o.endpoints, endpoint: new_spec})
388
+ save(new)
389
+ apply(o=new)
agent/router/reload.py ADDED
@@ -0,0 +1,28 @@
1
+ """One call after keys, endpoints or pins changed in the running process.
2
+
3
+ Three things have to happen, in this order, and every front end used to be
4
+ one forgotten step away from a stale router:
5
+
6
+ 1. `overrides.apply()` -- re-register custom endpoints and rebuild the
7
+ pinned chains in the live table;
8
+ 2. `llm_provider.reset()` -- drop cached provider instances and classes, so
9
+ a new key or a new base URL is read on the next construction;
10
+ 3. `Router.reset_all()` -- every live router re-snapshots which providers
11
+ are configured (agent/pipeline/nodes.py's module-level one included).
12
+
13
+ Precondition: call it between turns. A graph thread mid-resolve sees only
14
+ whole-tuple swaps, so nothing tears, but a turn already handed its model a
15
+ key it would be confusing to change under it.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ from agent.router import llm_provider, overrides
20
+ from agent.router.router import Router
21
+
22
+
23
+ def reload_everything() -> list[str]:
24
+ """Returns the problems `overrides.apply()` reported, for display."""
25
+ problems = overrides.apply()
26
+ llm_provider.reset()
27
+ Router.reset_all()
28
+ return problems