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
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)