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.
- agent/README.md +22 -0
- agent/__init__.py +0 -0
- agent/cli/README.md +77 -0
- agent/cli/__init__.py +0 -0
- agent/cli/art.py +371 -0
- agent/cli/chat.py +309 -0
- agent/cli/clipboard.py +106 -0
- agent/cli/context.py +56 -0
- agent/cli/doctor.py +60 -0
- agent/cli/errors.py +58 -0
- agent/cli/eval.py +130 -0
- agent/cli/eval_claw.py +414 -0
- agent/cli/eval_compaction.py +106 -0
- agent/cli/eval_hle.py +92 -0
- agent/cli/eval_memory.py +253 -0
- agent/cli/eval_swe.py +172 -0
- agent/cli/lessons.py +97 -0
- agent/cli/main.py +67 -0
- agent/cli/modals.py +570 -0
- agent/cli/models.py +64 -0
- agent/cli/output.py +54 -0
- agent/cli/route.py +78 -0
- agent/cli/sessions.py +125 -0
- agent/cli/setup_screen.py +562 -0
- agent/cli/shell.py +548 -0
- agent/cli/tui.py +1807 -0
- agent/cli/ui.py +14 -0
- agent/cli/usage_panel.py +159 -0
- agent/config/README.md +7 -0
- agent/config/__init__.py +0 -0
- agent/config/envfile.py +76 -0
- agent/eval/README.md +76 -0
- agent/eval/__init__.py +0 -0
- agent/eval/claw_bench.py +1031 -0
- agent/eval/compaction_bench.py +229 -0
- agent/eval/data/README.md +10 -0
- agent/eval/data/claw/README.md +108 -0
- agent/eval/data/claw/llm_judge-gemini.patch +57 -0
- agent/eval/data/claw/otto.yaml +35 -0
- agent/eval/failures.py +276 -0
- agent/eval/golden/README.md +33 -0
- agent/eval/golden/code_01.json +6 -0
- agent/eval/golden/code_02.json +6 -0
- agent/eval/golden/code_03.json +6 -0
- agent/eval/golden/code_04.json +6 -0
- agent/eval/golden/code_05.json +6 -0
- agent/eval/golden/code_06.json +6 -0
- agent/eval/golden/math_01.json +6 -0
- agent/eval/golden/math_02.json +6 -0
- agent/eval/golden/math_03.json +6 -0
- agent/eval/golden/math_04.json +6 -0
- agent/eval/golden/math_05.json +6 -0
- agent/eval/golden/math_06.json +6 -0
- agent/eval/golden/nphard_gcp_01.json +6 -0
- agent/eval/golden/nphard_ksp_01.json +6 -0
- agent/eval/golden/nphard_math_binpacking_01.json +6 -0
- agent/eval/golden/nphard_math_clique_01.json +6 -0
- agent/eval/golden/nphard_math_setcover_01.json +6 -0
- agent/eval/golden/nphard_math_subsetsum_01.json +6 -0
- agent/eval/golden/nphard_tsp_01.json +6 -0
- agent/eval/golden/nphard_tsp_02.json +6 -0
- agent/eval/hle_bench.py +273 -0
- agent/eval/langfuse_sync.py +172 -0
- agent/eval/memory_bench.py +538 -0
- agent/eval/runner.py +174 -0
- agent/eval/single_agent.py +120 -0
- agent/eval/swe_bench.py +604 -0
- agent/eval/terminal_bench.py +345 -0
- agent/memory/README.md +102 -0
- agent/memory/__init__.py +42 -0
- agent/memory/embeddings.py +302 -0
- agent/memory/hashing.py +15 -0
- agent/memory/lessons.py +483 -0
- agent/memory/queue.py +531 -0
- agent/memory/retrieval.py +493 -0
- agent/memory/session.py +60 -0
- agent/memory/sessions.py +436 -0
- agent/memory/store.py +429 -0
- agent/memory/tokens.py +60 -0
- agent/memory/wiring.py +146 -0
- agent/pipeline/README.md +135 -0
- agent/pipeline/__init__.py +0 -0
- agent/pipeline/browsing.py +609 -0
- agent/pipeline/budget.py +403 -0
- agent/pipeline/codemap.py +254 -0
- agent/pipeline/evidence.py +325 -0
- agent/pipeline/execution.py +67 -0
- agent/pipeline/modes.py +137 -0
- agent/pipeline/native.py +1137 -0
- agent/pipeline/nodes.py +3644 -0
- agent/pipeline/pricing.py +209 -0
- agent/pipeline/progress.py +139 -0
- agent/pipeline/rag.py +139 -0
- agent/pipeline/research.py +1325 -0
- agent/pipeline/run.py +528 -0
- agent/pipeline/screen.py +77 -0
- agent/pipeline/state.py +220 -0
- agent/pipeline/toolkit.py +328 -0
- agent/pipeline/tools.py +1990 -0
- agent/pipeline/tracing.py +147 -0
- agent/pipeline/usage.py +251 -0
- agent/pipeline/vision.py +84 -0
- agent/pipeline/walkthrough.py +735 -0
- agent/pipeline/workspace.py +229 -0
- agent/router/README.md +60 -0
- agent/router/__init__.py +0 -0
- agent/router/automap.py +114 -0
- agent/router/health.py +229 -0
- agent/router/llm_provider/README.md +38 -0
- agent/router/llm_provider/__init__.py +202 -0
- agent/router/llm_provider/anthropic_provider.py +128 -0
- agent/router/llm_provider/base.py +507 -0
- agent/router/llm_provider/custom.py +152 -0
- agent/router/llm_provider/gemini_provider.py +122 -0
- agent/router/llm_provider/inception_provider.py +687 -0
- agent/router/llm_provider/openai_provider.py +151 -0
- agent/router/llm_provider/retired.py +145 -0
- agent/router/llm_provider/temperature.py +371 -0
- agent/router/mapping.py +579 -0
- agent/router/outcomes.py +363 -0
- agent/router/overrides.py +389 -0
- agent/router/reload.py +28 -0
- agent/router/router.py +413 -0
- agent/router/setup.py +123 -0
- otto_cli_agent-0.1.0.dist-info/METADATA +115 -0
- otto_cli_agent-0.1.0.dist-info/RECORD +129 -0
- otto_cli_agent-0.1.0.dist-info/WHEEL +4 -0
- otto_cli_agent-0.1.0.dist-info/entry_points.txt +2 -0
- otto_cli_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
agent/cli/route.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
from rich.tree import Tree
|
|
3
|
+
from rich.panel import Panel
|
|
4
|
+
from rich.table import Table
|
|
5
|
+
from rich import box
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from agent.cli.ui import err, out
|
|
9
|
+
from agent.router import overrides as route_overrides
|
|
10
|
+
from agent.router.mapping import TASK_ROUTES, Task
|
|
11
|
+
from agent.router.outcomes import MIN_SAMPLES, records
|
|
12
|
+
from agent.router.router import NoViableRoute, render
|
|
13
|
+
|
|
14
|
+
def route(ctx: typer.Context,
|
|
15
|
+
task: Annotated[Task, typer.Argument(help="Task to resolve.")]
|
|
16
|
+
) -> None:
|
|
17
|
+
"""Show how a task resolves, and what it skipped to get there."""
|
|
18
|
+
try:
|
|
19
|
+
d = ctx.obj.router.resolve(task)
|
|
20
|
+
except NoViableRoute as exc:
|
|
21
|
+
out.print(_chain(task, exc.skipped, None))
|
|
22
|
+
err.print("[bad]no viable model[/]")
|
|
23
|
+
raise typer.Exit(1)
|
|
24
|
+
out.print(_chain(task, d.skipped, d))
|
|
25
|
+
out.print(_summary(d))
|
|
26
|
+
_print_evidence(task)
|
|
27
|
+
for problem in route_overrides.last_problems():
|
|
28
|
+
err.print(f"[warn]{problem}[/]")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _print_evidence(task: Task) -> None:
|
|
32
|
+
"""What this installation has observed each candidate achieve.
|
|
33
|
+
|
|
34
|
+
Printed under the chain because it is the reason the chain may not be in
|
|
35
|
+
the order mapping.py declares it. A route whose order came from evidence
|
|
36
|
+
and does not say so is a route nobody can debug.
|
|
37
|
+
"""
|
|
38
|
+
seen = records(task.value)
|
|
39
|
+
if not seen:
|
|
40
|
+
return
|
|
41
|
+
t = Table(box=box.SIMPLE, pad_edge=False)
|
|
42
|
+
t.add_column("model"); t.add_column("runs", justify="right")
|
|
43
|
+
t.add_column("approved", justify="right"); t.add_column("calls/run", justify="right")
|
|
44
|
+
for r in seen:
|
|
45
|
+
mark = "" if r.trusted else f" [muted](under {MIN_SAMPLES}, not used)[/]"
|
|
46
|
+
t.add_row(r.model_id + mark, str(r.runs),
|
|
47
|
+
f"{r.approval_rate:.0%}", f"{r.calls_per_run:.1f}")
|
|
48
|
+
out.print(t)
|
|
49
|
+
|
|
50
|
+
def _chain(task, skipped, d) -> Tree:
|
|
51
|
+
tree = Tree(f"[bold]{task.value}[/]")
|
|
52
|
+
chosen = d.index if d else None
|
|
53
|
+
for i, c in enumerate(TASK_ROUTES[task]):
|
|
54
|
+
pin = " (your pin)" if route_overrides.is_pin(task, c) else ""
|
|
55
|
+
if chosen is not None and i == chosen:
|
|
56
|
+
tree.add(f"[ok]✓[/] [chosen][{i}] {d.provider}:{d.model.id}{pin}[/]")
|
|
57
|
+
elif i < len(skipped):
|
|
58
|
+
sk = skipped[i]
|
|
59
|
+
tree.add(f"[bad]✗[/] [muted][{i}] {sk.target} — {sk.reason}[/]")
|
|
60
|
+
else:
|
|
61
|
+
tree.add(f"[muted]· [{i}] {render(c)}{pin} (not reached)[/]")
|
|
62
|
+
return tree
|
|
63
|
+
|
|
64
|
+
def _summary(d) -> Panel:
|
|
65
|
+
t = Table(box=None, show_header=False, pad_edge=False)
|
|
66
|
+
t.add_column(style="muted")
|
|
67
|
+
t.add_column()
|
|
68
|
+
t.add_row("model", f"[spec]{d.provider}:{d.model.id}[/]")
|
|
69
|
+
t.add_row("endpoint", d.endpoint.value)
|
|
70
|
+
t.add_row("context", f"{d.model.context_window:,}" if d.model.context_window else "—")
|
|
71
|
+
t.add_row("params", ", ".join(f"{k}={v}" for k, v in d.params.items()) or "—")
|
|
72
|
+
if route_overrides.pinned_spec(d.task) and d.index == 0:
|
|
73
|
+
t.add_row("state", f"[chosen]pinned[/] in {route_overrides.routes_path()}")
|
|
74
|
+
elif d.chosen_on_evidence:
|
|
75
|
+
t.add_row("state", f"[ok]chosen on observed results[/] — candidate {d.index}")
|
|
76
|
+
elif d.fell_back:
|
|
77
|
+
t.add_row("state", f"[warn]degraded — candidate {d.index}, {len(d.skipped)} skipped[/]")
|
|
78
|
+
return Panel(t, box=box.ROUNDED, border_style="muted", expand=False)
|
agent/cli/sessions.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""`otto sessions` -- list, delete, rename and prune saved sessions.
|
|
2
|
+
|
|
3
|
+
The index itself is agent/memory/sessions.py; this is the table both the
|
|
4
|
+
REPL's `/sessions` and this command print, plus the flags that change the
|
|
5
|
+
index from outside a session. Resuming is `otto chat --resume` / `otto tui
|
|
6
|
+
--resume` (and `/resume` inside either), since resuming means opening one.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from datetime import date
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
import typer
|
|
15
|
+
from rich import box
|
|
16
|
+
from rich.table import Table
|
|
17
|
+
from typing_extensions import Annotated
|
|
18
|
+
|
|
19
|
+
from agent.cli.ui import err, out
|
|
20
|
+
from agent.memory import sessions as session_index
|
|
21
|
+
from agent.memory.sessions import SessionInfo
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def sessions_table(rows: list[SessionInfo], current: str | None = None) -> Table:
|
|
25
|
+
"""Primitive Rich styles only, like agent/cli/lessons.py's table, so it
|
|
26
|
+
renders the same in the REPL and inside a Textual Static."""
|
|
27
|
+
table = Table(box=box.SIMPLE, show_edge=False, pad_edge=False)
|
|
28
|
+
table.add_column("id", style="bold", no_wrap=True)
|
|
29
|
+
table.add_column("title", overflow="fold", ratio=1)
|
|
30
|
+
table.add_column("turns", justify="right")
|
|
31
|
+
table.add_column("workspace", style="dim", overflow="fold")
|
|
32
|
+
table.add_column("last active", style="dim", no_wrap=True)
|
|
33
|
+
for r in rows:
|
|
34
|
+
mark = " ◂" if r.id == current else ""
|
|
35
|
+
workspace = r.workspace.rsplit("/", 1)[-1] if r.workspace else "off"
|
|
36
|
+
table.add_row(r.short_id + mark, r.label, str(r.turns), workspace,
|
|
37
|
+
session_index.describe_age(r.last_active_at))
|
|
38
|
+
return table
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def default_export_path(info: SessionInfo, directory: Path | None = None) -> Path:
|
|
42
|
+
"""`otto-session-<id>-<date>.json`, in `directory` (the current one by
|
|
43
|
+
default; the TUI proposes the home directory) -- the shape agent/cli/
|
|
44
|
+
lessons.py's export uses, with the id so two exports do not collide."""
|
|
45
|
+
return (directory or Path.cwd()) / f"otto-session-{info.short_id}-{date.today():%Y-%m-%d}.json"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def sessions_cmd(
|
|
49
|
+
delete: Annotated[
|
|
50
|
+
Optional[str],
|
|
51
|
+
typer.Option("--delete", help="Forget this session (id, prefix, or last): its row and its memory file."),
|
|
52
|
+
] = None,
|
|
53
|
+
rename: Annotated[
|
|
54
|
+
Optional[str],
|
|
55
|
+
typer.Option("--rename", help="Session to retitle (id, prefix, or last); needs --title."),
|
|
56
|
+
] = None,
|
|
57
|
+
title: Annotated[Optional[str], typer.Option("--title", help="The new title, with --rename.")] = None,
|
|
58
|
+
prune: Annotated[
|
|
59
|
+
bool,
|
|
60
|
+
typer.Option("--prune", help="Delete memory files no session owns and nothing was written to, "
|
|
61
|
+
"and rows whose file is gone."),
|
|
62
|
+
] = False,
|
|
63
|
+
export: Annotated[
|
|
64
|
+
Optional[str],
|
|
65
|
+
typer.Option("--export", help="Session to write out as JSON (id, prefix, or last); --to says where."),
|
|
66
|
+
] = None,
|
|
67
|
+
to: Annotated[Optional[Path], typer.Option("--to", help="With --export: the file to write.")] = None,
|
|
68
|
+
import_from: Annotated[
|
|
69
|
+
Optional[Path],
|
|
70
|
+
typer.Option("--import", help="Read a session export into this machine's sessions."),
|
|
71
|
+
] = None,
|
|
72
|
+
limit: Annotated[int, typer.Option("--limit", help="How many to list.")] = 30,
|
|
73
|
+
) -> None:
|
|
74
|
+
if export is not None:
|
|
75
|
+
info = _resolve_or_exit(export)
|
|
76
|
+
written = session_index.export_session(info.id, to or default_export_path(info))
|
|
77
|
+
# soft_wrap: a path someone will copy must not be broken across lines
|
|
78
|
+
# by the console width -- Rich would split it mid-name in a narrow
|
|
79
|
+
# terminal, and did in CI.
|
|
80
|
+
err.print(f"exported {info.short_id} · {info.label} to {written}", soft_wrap=True)
|
|
81
|
+
return
|
|
82
|
+
if import_from is not None:
|
|
83
|
+
try:
|
|
84
|
+
info = session_index.import_session(import_from)
|
|
85
|
+
except ValueError as exc:
|
|
86
|
+
err.print(str(exc))
|
|
87
|
+
raise typer.Exit(1)
|
|
88
|
+
err.print(f"imported {info.short_id} · {info.label} · {info.turns} turn(s); "
|
|
89
|
+
f"otto chat --resume {info.short_id}")
|
|
90
|
+
return
|
|
91
|
+
if delete is not None:
|
|
92
|
+
info = _resolve_or_exit(delete)
|
|
93
|
+
try:
|
|
94
|
+
session_index.delete(info.id)
|
|
95
|
+
except OSError as exc:
|
|
96
|
+
err.print(str(exc))
|
|
97
|
+
raise typer.Exit(1)
|
|
98
|
+
err.print(f"deleted {info.short_id} · {info.label}")
|
|
99
|
+
return
|
|
100
|
+
if rename is not None:
|
|
101
|
+
if not title:
|
|
102
|
+
err.print("--rename needs --title")
|
|
103
|
+
raise typer.Exit(2)
|
|
104
|
+
info = _resolve_or_exit(rename)
|
|
105
|
+
session_index.rename(info.id, title)
|
|
106
|
+
err.print(f"{info.short_id} is now called {title!r}")
|
|
107
|
+
return
|
|
108
|
+
if prune:
|
|
109
|
+
report = session_index.prune()
|
|
110
|
+
err.print(report.summary())
|
|
111
|
+
return
|
|
112
|
+
rows = session_index.list_sessions(limit=limit)
|
|
113
|
+
if not rows:
|
|
114
|
+
out.print("no saved sessions yet -- a session is saved once a turn finishes")
|
|
115
|
+
return
|
|
116
|
+
out.print(sessions_table(rows))
|
|
117
|
+
out.print("otto chat --resume <id> · otto tui --resume <id> · last is the newest")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _resolve_or_exit(ref: str) -> SessionInfo:
|
|
121
|
+
try:
|
|
122
|
+
return session_index.resolve(ref)
|
|
123
|
+
except LookupError as exc:
|
|
124
|
+
err.print(str(exc))
|
|
125
|
+
raise typer.Exit(1)
|