simulai-cli 0.1.1__tar.gz → 2.1.0__tar.gz
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.
- simulai_cli-2.1.0/PKG-INFO +89 -0
- simulai_cli-2.1.0/README.md +63 -0
- {simulai_cli-0.1.1 → simulai_cli-2.1.0}/pyproject.toml +47 -45
- simulai_cli-2.1.0/simulai_cli/__init__.py +1 -0
- simulai_cli-2.1.0/simulai_cli/agent.py +403 -0
- simulai_cli-2.1.0/simulai_cli/api.py +545 -0
- simulai_cli-2.1.0/simulai_cli/config.py +144 -0
- simulai_cli-2.1.0/simulai_cli/discovery.py +295 -0
- simulai_cli-2.1.0/simulai_cli/main.py +1051 -0
- simulai_cli-2.1.0/simulai_cli/render.py +219 -0
- simulai_cli-2.1.0/simulai_cli/tools.py +571 -0
- simulai_cli-2.1.0/simulai_cli.egg-info/PKG-INFO +89 -0
- simulai_cli-2.1.0/simulai_cli.egg-info/SOURCES.txt +16 -0
- {simulai_cli-0.1.1 → simulai_cli-2.1.0}/simulai_cli.egg-info/requires.txt +0 -1
- simulai_cli-0.1.1/PKG-INFO +0 -71
- simulai_cli-0.1.1/README.md +0 -44
- simulai_cli-0.1.1/simulai_cli/__init__.py +0 -0
- simulai_cli-0.1.1/simulai_cli/main.py +0 -690
- simulai_cli-0.1.1/simulai_cli.egg-info/PKG-INFO +0 -71
- simulai_cli-0.1.1/simulai_cli.egg-info/SOURCES.txt +0 -16
- {simulai_cli-0.1.1 → simulai_cli-2.1.0}/setup.cfg +0 -0
- {simulai_cli-0.1.1 → simulai_cli-2.1.0}/simulai_cli.egg-info/dependency_links.txt +0 -0
- {simulai_cli-0.1.1 → simulai_cli-2.1.0}/simulai_cli.egg-info/entry_points.txt +0 -0
- {simulai_cli-0.1.1 → simulai_cli-2.1.0}/simulai_cli.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: simulai-cli
|
|
3
|
+
Version: 2.1.0
|
|
4
|
+
Summary: SimulAI CLI — persistent AI chat in your terminal. SmartRoute across 20+ models, Naira billing, and import your Claude Code / Codex sessions.
|
|
5
|
+
Author-email: Simul AI <hello@trysimulai.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://trysimulai.com
|
|
8
|
+
Project-URL: Documentation, https://docs.trysimulai.com
|
|
9
|
+
Project-URL: Repository, https://github.com/Gracemansam/simulai_client
|
|
10
|
+
Keywords: ai,llm,cli,nigeria,openai,claude,gemini,claude-code,codex,coding-agent,smartroute
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
Requires-Dist: typer>=0.9.0
|
|
24
|
+
Requires-Dist: rich>=13.0.0
|
|
25
|
+
Requires-Dist: httpx>=0.24.0
|
|
26
|
+
|
|
27
|
+
# SimulAI CLI v2
|
|
28
|
+
|
|
29
|
+
Persistent AI chat in your terminal. SmartRoute across 20+ models, Naira
|
|
30
|
+
billing, and one-command import of your Claude Code / Codex CLI sessions.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install simulai-cli
|
|
36
|
+
simulai login
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Core ideas
|
|
40
|
+
|
|
41
|
+
**Your conversation follows you.** Every exchange is saved server-side, so
|
|
42
|
+
a thread started in the terminal continues in the web app and vice versa.
|
|
43
|
+
Inside a repo, `simulai chat` resumes that repo's conversation automatically.
|
|
44
|
+
|
|
45
|
+
**SmartRoute by default, your call any time.** The same router the web
|
|
46
|
+
dashboard uses picks a model per message. `/model anthropic/claude-sonnet-4`
|
|
47
|
+
takes over whenever you want, and the choice persists.
|
|
48
|
+
|
|
49
|
+
**Hit a limit in another tool? Bring the session with you.**
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
cd ~/code/my-api
|
|
53
|
+
simulai import claude-code
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Shows *this repo's* sessions from the last two weeks — not a month of every
|
|
57
|
+
project you've touched. Pick one, and it lands in SimulAI as a normal
|
|
58
|
+
conversation you can continue on any model.
|
|
59
|
+
|
|
60
|
+
## Commands
|
|
61
|
+
|
|
62
|
+
| Command | What it does |
|
|
63
|
+
|---|---|
|
|
64
|
+
| `simulai login` | Log in (unlocks everything) |
|
|
65
|
+
| `simulai code "task"` | **Agentic coding** — reads, greps, edits and tests this repo |
|
|
66
|
+
| `simulai chat` | Resume this directory's conversation |
|
|
67
|
+
| `simulai chat "msg"` | One-shot |
|
|
68
|
+
| `simulai chat -m <model>` | Force a specific model |
|
|
69
|
+
| `simulai chat --web` | Live web search |
|
|
70
|
+
| `simulai chat -f file.py` | Attach a file |
|
|
71
|
+
| `simulai chats` | List conversations |
|
|
72
|
+
| `simulai resume` | Pick one and bind it here |
|
|
73
|
+
| `simulai import claude-code` | Import a Claude Code session from this repo |
|
|
74
|
+
| `simulai import codex` | Same for Codex CLI |
|
|
75
|
+
| `simulai import scan` | Check every supported agent |
|
|
76
|
+
| `simulai import file x.jsonl` | Import an exported transcript |
|
|
77
|
+
| `simulai import link <url>` | Import a ChatGPT/Claude share link |
|
|
78
|
+
| `simulai analyze file.py` | Review a file |
|
|
79
|
+
| `simulai models [query]` | Browse models |
|
|
80
|
+
| `simulai wallet` | Credit balance |
|
|
81
|
+
| `simulai usage` | Credit ledger — what you spent, on what |
|
|
82
|
+
| `simulai config effort max` | Change a default |
|
|
83
|
+
|
|
84
|
+
## In-chat slash commands
|
|
85
|
+
|
|
86
|
+
`/model` `/models` `/smart` `/effort` `/web` `/mode` `/project` `/new`
|
|
87
|
+
`/title` `/code` `/save` `/file` `/cost` `/usage` `/trim` `/clear` `/exit`
|
|
88
|
+
|
|
89
|
+
Run `/help` for the full list.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# SimulAI CLI v2
|
|
2
|
+
|
|
3
|
+
Persistent AI chat in your terminal. SmartRoute across 20+ models, Naira
|
|
4
|
+
billing, and one-command import of your Claude Code / Codex CLI sessions.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install simulai-cli
|
|
10
|
+
simulai login
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Core ideas
|
|
14
|
+
|
|
15
|
+
**Your conversation follows you.** Every exchange is saved server-side, so
|
|
16
|
+
a thread started in the terminal continues in the web app and vice versa.
|
|
17
|
+
Inside a repo, `simulai chat` resumes that repo's conversation automatically.
|
|
18
|
+
|
|
19
|
+
**SmartRoute by default, your call any time.** The same router the web
|
|
20
|
+
dashboard uses picks a model per message. `/model anthropic/claude-sonnet-4`
|
|
21
|
+
takes over whenever you want, and the choice persists.
|
|
22
|
+
|
|
23
|
+
**Hit a limit in another tool? Bring the session with you.**
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
cd ~/code/my-api
|
|
27
|
+
simulai import claude-code
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Shows *this repo's* sessions from the last two weeks — not a month of every
|
|
31
|
+
project you've touched. Pick one, and it lands in SimulAI as a normal
|
|
32
|
+
conversation you can continue on any model.
|
|
33
|
+
|
|
34
|
+
## Commands
|
|
35
|
+
|
|
36
|
+
| Command | What it does |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `simulai login` | Log in (unlocks everything) |
|
|
39
|
+
| `simulai code "task"` | **Agentic coding** — reads, greps, edits and tests this repo |
|
|
40
|
+
| `simulai chat` | Resume this directory's conversation |
|
|
41
|
+
| `simulai chat "msg"` | One-shot |
|
|
42
|
+
| `simulai chat -m <model>` | Force a specific model |
|
|
43
|
+
| `simulai chat --web` | Live web search |
|
|
44
|
+
| `simulai chat -f file.py` | Attach a file |
|
|
45
|
+
| `simulai chats` | List conversations |
|
|
46
|
+
| `simulai resume` | Pick one and bind it here |
|
|
47
|
+
| `simulai import claude-code` | Import a Claude Code session from this repo |
|
|
48
|
+
| `simulai import codex` | Same for Codex CLI |
|
|
49
|
+
| `simulai import scan` | Check every supported agent |
|
|
50
|
+
| `simulai import file x.jsonl` | Import an exported transcript |
|
|
51
|
+
| `simulai import link <url>` | Import a ChatGPT/Claude share link |
|
|
52
|
+
| `simulai analyze file.py` | Review a file |
|
|
53
|
+
| `simulai models [query]` | Browse models |
|
|
54
|
+
| `simulai wallet` | Credit balance |
|
|
55
|
+
| `simulai usage` | Credit ledger — what you spent, on what |
|
|
56
|
+
| `simulai config effort max` | Change a default |
|
|
57
|
+
|
|
58
|
+
## In-chat slash commands
|
|
59
|
+
|
|
60
|
+
`/model` `/models` `/smart` `/effort` `/web` `/mode` `/project` `/new`
|
|
61
|
+
`/title` `/code` `/save` `/file` `/cost` `/usage` `/trim` `/clear` `/exit`
|
|
62
|
+
|
|
63
|
+
Run `/help` for the full list.
|
|
@@ -1,45 +1,47 @@
|
|
|
1
|
-
[build-system]
|
|
2
|
-
requires = ["setuptools>=61.0"]
|
|
3
|
-
build-backend = "setuptools.build_meta"
|
|
4
|
-
|
|
5
|
-
[project]
|
|
6
|
-
name = "simulai-cli"
|
|
7
|
-
version = "
|
|
8
|
-
description = "
|
|
9
|
-
readme = "README.md"
|
|
10
|
-
license = { text = "MIT" }
|
|
11
|
-
authors = [
|
|
12
|
-
{ name = "Simul AI", email = "hello@trysimulai.com" }
|
|
13
|
-
]
|
|
14
|
-
keywords = [
|
|
15
|
-
|
|
16
|
-
"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"Programming Language :: Python :: 3
|
|
24
|
-
"Programming Language :: Python :: 3.
|
|
25
|
-
"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "simulai-cli"
|
|
7
|
+
version = "2.1.0"
|
|
8
|
+
description = "SimulAI CLI — persistent AI chat in your terminal. SmartRoute across 20+ models, Naira billing, and import your Claude Code / Codex sessions."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Simul AI", email = "hello@trysimulai.com" }
|
|
13
|
+
]
|
|
14
|
+
keywords = [
|
|
15
|
+
"ai", "llm", "cli", "nigeria", "openai", "claude", "gemini",
|
|
16
|
+
"claude-code", "codex", "coding-agent", "smartroute",
|
|
17
|
+
]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 4 - Beta",
|
|
20
|
+
"Environment :: Console",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"License :: OSI Approved :: MIT License",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.9",
|
|
25
|
+
"Programming Language :: Python :: 3.10",
|
|
26
|
+
"Programming Language :: Python :: 3.11",
|
|
27
|
+
"Programming Language :: Python :: 3.12",
|
|
28
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
29
|
+
]
|
|
30
|
+
requires-python = ">=3.9"
|
|
31
|
+
dependencies = [
|
|
32
|
+
"typer>=0.9.0",
|
|
33
|
+
"rich>=13.0.0",
|
|
34
|
+
"httpx>=0.24.0",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.scripts]
|
|
38
|
+
simulai = "simulai_cli.main:app"
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Homepage = "https://trysimulai.com"
|
|
42
|
+
Documentation = "https://docs.trysimulai.com"
|
|
43
|
+
Repository = "https://github.com/Gracemansam/simulai_client"
|
|
44
|
+
|
|
45
|
+
[tool.setuptools.packages.find]
|
|
46
|
+
where = ["."]
|
|
47
|
+
include = ["simulai_cli*"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The `simulai code` agent loop.
|
|
3
|
+
|
|
4
|
+
user prompt
|
|
5
|
+
→ POST /v1/code/agent with the tool schema
|
|
6
|
+
→ model streams prose and/or tool calls
|
|
7
|
+
→ tools execute HERE, with approval for anything destructive
|
|
8
|
+
→ results go back as role:"tool" messages
|
|
9
|
+
→ repeat until the model stops asking for tools
|
|
10
|
+
|
|
11
|
+
Design decisions that matter for output quality:
|
|
12
|
+
|
|
13
|
+
• LOW TEMPERATURE (0.2, set server-side). Agent work wants determinism.
|
|
14
|
+
• EDITS ARE PREVIEWED AS DIFFS before they touch disk. You approve a diff,
|
|
15
|
+
not a promise.
|
|
16
|
+
• SYNTAX IS CHECKED BEFORE THE WRITE, and a failure is reported back to the
|
|
17
|
+
model as a tool error — so it repairs inside the loop instead of handing
|
|
18
|
+
you code that doesn't parse.
|
|
19
|
+
• A VERIFICATION PASS runs the project's tests after edits land, and
|
|
20
|
+
failures are fed back for up to two repair rounds. This is the single
|
|
21
|
+
biggest quality lever available without changing models.
|
|
22
|
+
• STEP CAP. Runaway loops burn credits; 25 steps is generous for real work
|
|
23
|
+
and cheap to raise.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import json
|
|
27
|
+
from typing import List, Optional
|
|
28
|
+
|
|
29
|
+
from rich.console import Console
|
|
30
|
+
from rich.panel import Panel
|
|
31
|
+
from rich.prompt import Prompt
|
|
32
|
+
from rich.syntax import Syntax
|
|
33
|
+
|
|
34
|
+
from . import render
|
|
35
|
+
from .api import ApiError, SimulAIClient
|
|
36
|
+
from .tools import (
|
|
37
|
+
DESTRUCTIVE,
|
|
38
|
+
TOOL_SCHEMA,
|
|
39
|
+
ToolExecutor,
|
|
40
|
+
Workspace,
|
|
41
|
+
detect_test_command,
|
|
42
|
+
git_status,
|
|
43
|
+
is_environment_failure,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
console = Console()
|
|
47
|
+
|
|
48
|
+
MAX_STEPS = 25
|
|
49
|
+
MAX_REPAIR_ROUNDS = 2
|
|
50
|
+
|
|
51
|
+
SYSTEM_PROMPT = """You are a coding agent working directly in a developer's repository.
|
|
52
|
+
|
|
53
|
+
Rules that matter:
|
|
54
|
+
- Investigate before you edit. Use grep to locate code and read_file with line
|
|
55
|
+
ranges to inspect it. Never guess at file contents or API signatures.
|
|
56
|
+
- Prefer edit_file over write_file for existing files. old_string must match
|
|
57
|
+
the file byte-for-byte and appear exactly once — include surrounding lines
|
|
58
|
+
to disambiguate.
|
|
59
|
+
- Make the smallest change that fixes the problem. Do not reformat, rename,
|
|
60
|
+
or "improve" code you were not asked to touch.
|
|
61
|
+
- If an edit is rejected for syntax or ambiguity, read the file again and fix
|
|
62
|
+
your input. Do not retry the identical call.
|
|
63
|
+
- After changing code, propose a command that verifies it (the test suite, a
|
|
64
|
+
type-checker, or the specific failing test).
|
|
65
|
+
- When you are done, state plainly what you changed and what the user should
|
|
66
|
+
check. Do not claim you ran something you did not run.
|
|
67
|
+
|
|
68
|
+
You cannot see the repository except through your tools. The user sees every
|
|
69
|
+
tool call and approves anything destructive."""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _tool_signature(name: str, args: dict) -> str:
|
|
73
|
+
"""A stable fingerprint, used to catch the model retrying the identical
|
|
74
|
+
failing call — a common and expensive loop."""
|
|
75
|
+
return f"{name}:{json.dumps(args, sort_keys=True)[:400]}"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def run_agent(
|
|
79
|
+
client: SimulAIClient,
|
|
80
|
+
prompt: str,
|
|
81
|
+
root: str,
|
|
82
|
+
model: Optional[str] = None,
|
|
83
|
+
auto_approve: bool = False,
|
|
84
|
+
dry_run: bool = False,
|
|
85
|
+
effort: str = "balanced",
|
|
86
|
+
verify: bool = True,
|
|
87
|
+
) -> None:
|
|
88
|
+
workspace = Workspace(root)
|
|
89
|
+
executor = ToolExecutor(workspace, auto_approve=auto_approve, dry_run=dry_run)
|
|
90
|
+
|
|
91
|
+
git = git_status(root)
|
|
92
|
+
if git["repo"] and git["dirty"] and not dry_run and not auto_approve:
|
|
93
|
+
console.print(
|
|
94
|
+
f"[yellow]⚠ Uncommitted changes on branch '{git['branch']}'.[/yellow] "
|
|
95
|
+
"[dim]Commit or stash first so you can undo cleanly.[/dim]"
|
|
96
|
+
)
|
|
97
|
+
if Prompt.ask("Continue anyway?", choices=["y", "n"], default="n") == "n":
|
|
98
|
+
return
|
|
99
|
+
if not git["repo"]:
|
|
100
|
+
console.print(
|
|
101
|
+
"[yellow]⚠ Not a git repository — edits cannot be rolled back with git.[/yellow]"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
messages: List[dict] = [
|
|
105
|
+
{"role": "system", "content": SYSTEM_PROMPT},
|
|
106
|
+
{"role": "user", "content": f"Workspace root: {root}\n\n{prompt}"},
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
seen_calls: set = set()
|
|
110
|
+
steps = 0
|
|
111
|
+
repair_rounds = 0
|
|
112
|
+
tokens_in = tokens_out = 0
|
|
113
|
+
|
|
114
|
+
while steps < MAX_STEPS:
|
|
115
|
+
steps += 1
|
|
116
|
+
text, tool_calls, usage = _stream_turn(client, messages, model, effort)
|
|
117
|
+
tokens_in += usage.get("input", 0)
|
|
118
|
+
tokens_out += usage.get("output", 0)
|
|
119
|
+
|
|
120
|
+
# NOTE: no markdown re-render here. _stream_turn already printed the
|
|
121
|
+
# text live as it arrived; rendering it again duplicated every final
|
|
122
|
+
# message. Live streaming wins over syntax highlighting in an agent
|
|
123
|
+
# run — the substance is in the diffs, which are highlighted, and
|
|
124
|
+
# prose between tool calls is short.
|
|
125
|
+
|
|
126
|
+
# ── Tool-capability check ────────────────────────────────────────
|
|
127
|
+
# If the very first turn comes back with no tool calls at all, the
|
|
128
|
+
# routed model almost certainly cannot call functions — OpenRouter
|
|
129
|
+
# silently drops the `tools` field for models that do not support
|
|
130
|
+
# it, so the model just answers in prose (usually by asking the user
|
|
131
|
+
# a question it could have answered with grep). The catalog has no
|
|
132
|
+
# supports_tools flag to filter on, so detect it here and say so
|
|
133
|
+
# plainly rather than letting the user think the agent is broken.
|
|
134
|
+
if steps == 1 and not tool_calls and executor.files_changed == []:
|
|
135
|
+
console.print(
|
|
136
|
+
"\n[yellow]⚠ The model made no tool calls.[/yellow] "
|
|
137
|
+
"[dim]It likely doesn't support function calling.[/dim]\n"
|
|
138
|
+
" Retry with a tool-capable model:\n"
|
|
139
|
+
" [bold]simulai models[/bold] then "
|
|
140
|
+
"[bold]simulai code -m <model-id> \"...\"[/bold]\n"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
if not tool_calls:
|
|
144
|
+
# Model answered in prose — either it's done, or verification
|
|
145
|
+
# found problems worth one more round.
|
|
146
|
+
if verify and executor.files_changed and repair_rounds < MAX_REPAIR_ROUNDS:
|
|
147
|
+
failure = _verify(executor, root)
|
|
148
|
+
if failure:
|
|
149
|
+
repair_rounds += 1
|
|
150
|
+
console.print(
|
|
151
|
+
f"[yellow]Verification failed — asking for a fix "
|
|
152
|
+
f"(round {repair_rounds}/{MAX_REPAIR_ROUNDS})[/yellow]"
|
|
153
|
+
)
|
|
154
|
+
messages.append({"role": "assistant", "content": text or "(done)"})
|
|
155
|
+
messages.append(
|
|
156
|
+
{
|
|
157
|
+
"role": "user",
|
|
158
|
+
"content": (
|
|
159
|
+
"The test suite failed after your changes.\n\n"
|
|
160
|
+
"Fix the SOURCE CODE you changed. Do not:\n"
|
|
161
|
+
"- install packages or modify the environment\n"
|
|
162
|
+
"- add config files, wrappers or scripts\n"
|
|
163
|
+
"- change or delete the test to make it pass\n"
|
|
164
|
+
"If the failure is unrelated to your edit, say so "
|
|
165
|
+
"plainly and stop.\n\n"
|
|
166
|
+
f"{failure}"
|
|
167
|
+
),
|
|
168
|
+
}
|
|
169
|
+
)
|
|
170
|
+
continue
|
|
171
|
+
break
|
|
172
|
+
|
|
173
|
+
# Persist the assistant's tool-call turn exactly as the API expects.
|
|
174
|
+
messages.append(
|
|
175
|
+
{
|
|
176
|
+
"role": "assistant",
|
|
177
|
+
"content": text or None,
|
|
178
|
+
"tool_calls": [
|
|
179
|
+
{
|
|
180
|
+
"id": call["id"],
|
|
181
|
+
"type": "function",
|
|
182
|
+
"function": {
|
|
183
|
+
"name": call["name"],
|
|
184
|
+
"arguments": call["arguments"],
|
|
185
|
+
},
|
|
186
|
+
}
|
|
187
|
+
for call in tool_calls
|
|
188
|
+
],
|
|
189
|
+
}
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
for call in tool_calls:
|
|
193
|
+
result_text = _execute(executor, call, seen_calls)
|
|
194
|
+
messages.append(
|
|
195
|
+
{
|
|
196
|
+
"role": "tool",
|
|
197
|
+
"tool_call_id": call["id"],
|
|
198
|
+
"content": result_text,
|
|
199
|
+
}
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
if steps >= MAX_STEPS:
|
|
203
|
+
console.print(f"[yellow]Stopped at the {MAX_STEPS}-step cap.[/yellow]")
|
|
204
|
+
|
|
205
|
+
_summary(executor, tokens_in, tokens_out, git)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _stream_turn(
|
|
209
|
+
client: SimulAIClient, messages: List[dict], model: Optional[str], effort: str
|
|
210
|
+
) -> tuple:
|
|
211
|
+
"""One request. Returns (text, tool_calls, usage)."""
|
|
212
|
+
text = ""
|
|
213
|
+
tool_calls: List[dict] = []
|
|
214
|
+
usage = {"input": 0, "output": 0}
|
|
215
|
+
|
|
216
|
+
console.print()
|
|
217
|
+
try:
|
|
218
|
+
for event in client.stream_agent(
|
|
219
|
+
messages=messages, tools=TOOL_SCHEMA, model=model, effort=effort
|
|
220
|
+
):
|
|
221
|
+
kind = event["type"]
|
|
222
|
+
if kind == "text":
|
|
223
|
+
text += event["text"]
|
|
224
|
+
console.print(event["text"], end="")
|
|
225
|
+
elif kind == "tool_calls":
|
|
226
|
+
tool_calls = event["tool_calls"]
|
|
227
|
+
elif kind == "usage":
|
|
228
|
+
usage["input"] += event.get("input", 0)
|
|
229
|
+
usage["output"] += event.get("output", 0)
|
|
230
|
+
elif kind == "done":
|
|
231
|
+
break
|
|
232
|
+
except ApiError as exc:
|
|
233
|
+
render.error(exc.message)
|
|
234
|
+
return "", [], usage
|
|
235
|
+
except KeyboardInterrupt:
|
|
236
|
+
console.print("\n[dim]interrupted[/dim]")
|
|
237
|
+
return text, [], usage
|
|
238
|
+
|
|
239
|
+
console.print()
|
|
240
|
+
return text, tool_calls, usage
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _execute(executor: ToolExecutor, call: dict, seen: set) -> str:
|
|
244
|
+
"""Run one tool call, prompting when destructive. Returns model-facing text."""
|
|
245
|
+
name = call["name"]
|
|
246
|
+
try:
|
|
247
|
+
args = json.loads(call["arguments"] or "{}")
|
|
248
|
+
except json.JSONDecodeError:
|
|
249
|
+
return "Error: your tool arguments were not valid JSON. Resend them correctly."
|
|
250
|
+
|
|
251
|
+
signature = _tool_signature(name, args)
|
|
252
|
+
if signature in seen and name in DESTRUCTIVE:
|
|
253
|
+
return (
|
|
254
|
+
"Error: you already made this exact call and it failed. "
|
|
255
|
+
"Read the current file contents before trying again."
|
|
256
|
+
)
|
|
257
|
+
seen.add(signature)
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
# ── read-only tools: run immediately ──────────────────────────────
|
|
261
|
+
if name == "read_file":
|
|
262
|
+
result = executor.read_file(
|
|
263
|
+
args.get("path", ""), args.get("start_line"), args.get("end_line")
|
|
264
|
+
)
|
|
265
|
+
elif name == "list_files":
|
|
266
|
+
result = executor.list_files(args.get("path", "."), args.get("depth", 2))
|
|
267
|
+
elif name == "grep":
|
|
268
|
+
result = executor.grep(
|
|
269
|
+
args.get("pattern", ""), args.get("path", "."), args.get("glob")
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# ── destructive tools: preview, approve, then apply ───────────────
|
|
273
|
+
elif name == "edit_file":
|
|
274
|
+
result, diff = executor.edit_file(
|
|
275
|
+
args.get("path", ""), args.get("old_string", ""), args.get("new_string", "")
|
|
276
|
+
)
|
|
277
|
+
if not result.ok:
|
|
278
|
+
console.print(f" [red]✗ {name} {args.get('path','')}[/red]")
|
|
279
|
+
return result.output
|
|
280
|
+
if not _approve(f"Edit {args.get('path')}", diff, executor):
|
|
281
|
+
return "The user declined this edit. Ask what they would prefer."
|
|
282
|
+
result = executor.apply_edit(
|
|
283
|
+
args.get("path", ""), args.get("old_string", ""), args.get("new_string", "")
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
elif name == "write_file":
|
|
287
|
+
result, diff = executor.write_file(args.get("path", ""), args.get("content", ""))
|
|
288
|
+
if not result.ok:
|
|
289
|
+
console.print(f" [red]✗ {name} {args.get('path','')}[/red]")
|
|
290
|
+
return result.output
|
|
291
|
+
if not _approve(f"Write {args.get('path')}", diff, executor):
|
|
292
|
+
return "The user declined this write. Ask what they would prefer."
|
|
293
|
+
result = executor.apply_write(args.get("path", ""), args.get("content", ""))
|
|
294
|
+
|
|
295
|
+
elif name == "run_command":
|
|
296
|
+
command = args.get("command", "")
|
|
297
|
+
reason = args.get("reason", "")
|
|
298
|
+
if not _approve(
|
|
299
|
+
f"Run: {command}" + (f"\n[dim]{reason}[/dim]" if reason else ""),
|
|
300
|
+
None,
|
|
301
|
+
executor,
|
|
302
|
+
):
|
|
303
|
+
return "The user declined to run this command."
|
|
304
|
+
result = executor.run_command(command, reason)
|
|
305
|
+
|
|
306
|
+
else:
|
|
307
|
+
return f"Error: unknown tool '{name}'."
|
|
308
|
+
|
|
309
|
+
except PermissionError as exc:
|
|
310
|
+
console.print(f" [red]✗ {exc}[/red]")
|
|
311
|
+
return f"Error: {exc}"
|
|
312
|
+
except Exception as exc:
|
|
313
|
+
console.print(f" [red]✗ {name} failed: {exc}[/red]")
|
|
314
|
+
return f"Error running {name}: {exc}"
|
|
315
|
+
|
|
316
|
+
marker = "[green]✓[/green]" if result.ok else "[red]✗[/red]"
|
|
317
|
+
console.print(f" {marker} [dim]{result.display or name}[/dim]")
|
|
318
|
+
return result.output or ("Done." if result.ok else "Failed.")
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _approve(title: str, diff: Optional[str], executor: ToolExecutor) -> bool:
|
|
322
|
+
if executor.auto_approve or executor.dry_run:
|
|
323
|
+
return True
|
|
324
|
+
console.print()
|
|
325
|
+
if diff:
|
|
326
|
+
console.print(
|
|
327
|
+
Panel(
|
|
328
|
+
Syntax(diff, "diff", theme="monokai", word_wrap=True),
|
|
329
|
+
title=title,
|
|
330
|
+
border_style="yellow",
|
|
331
|
+
)
|
|
332
|
+
)
|
|
333
|
+
else:
|
|
334
|
+
console.print(Panel(title, border_style="yellow"))
|
|
335
|
+
answer = Prompt.ask("Apply?", choices=["y", "n", "a"], default="y")
|
|
336
|
+
if answer == "a":
|
|
337
|
+
executor.auto_approve = True
|
|
338
|
+
console.print("[dim]Auto-approving the rest of this run.[/dim]")
|
|
339
|
+
return True
|
|
340
|
+
return answer == "y"
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _verify(executor: ToolExecutor, root: str) -> Optional[str]:
|
|
344
|
+
"""Run the project's tests. → failure text, or None if clean/unrunnable.
|
|
345
|
+
|
|
346
|
+
Three guards learned from a real run that went wrong:
|
|
347
|
+
|
|
348
|
+
1. If the model ALREADY ran the tests during the loop, don't run them
|
|
349
|
+
again. Re-running duplicates a billed round trip and, worse, can
|
|
350
|
+
contradict what the model just concluded.
|
|
351
|
+
2. If the command cannot RUN (not installed, not on PATH), that is an
|
|
352
|
+
environment problem, not a code problem. Report it and stop — do not
|
|
353
|
+
hand it to the model as a failure to fix.
|
|
354
|
+
3. Only genuine test failures trigger a repair round.
|
|
355
|
+
"""
|
|
356
|
+
command = detect_test_command(root)
|
|
357
|
+
if not command:
|
|
358
|
+
console.print(
|
|
359
|
+
"[dim]No test command detected — skipping verification. "
|
|
360
|
+
"Review the diff yourself.[/dim]"
|
|
361
|
+
)
|
|
362
|
+
return None
|
|
363
|
+
|
|
364
|
+
already = [c for c in executor.commands_run if "pytest" in c or "test" in c]
|
|
365
|
+
if already:
|
|
366
|
+
console.print(
|
|
367
|
+
f"[dim]Tests already run during the session ({already[-1][:60]}) "
|
|
368
|
+
"— skipping duplicate verification.[/dim]"
|
|
369
|
+
)
|
|
370
|
+
return None
|
|
371
|
+
|
|
372
|
+
console.print(f"\n[dim]Verifying with: {command}[/dim]")
|
|
373
|
+
result = executor.run_command(command, "post-edit verification")
|
|
374
|
+
|
|
375
|
+
if result.ok:
|
|
376
|
+
console.print(" [green]✓ tests pass[/green]")
|
|
377
|
+
return None
|
|
378
|
+
|
|
379
|
+
if is_environment_failure(result.output, 1):
|
|
380
|
+
console.print(
|
|
381
|
+
" [yellow]⚠ Verification could not run — the test tool isn't "
|
|
382
|
+
"installed or isn't on PATH.[/yellow]\n"
|
|
383
|
+
" [dim]This is an environment issue, not a problem with the "
|
|
384
|
+
"edit. Run your tests manually to confirm.[/dim]"
|
|
385
|
+
)
|
|
386
|
+
return None
|
|
387
|
+
|
|
388
|
+
console.print(" [red]✗ tests fail[/red]")
|
|
389
|
+
return result.output
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _summary(executor: ToolExecutor, tokens_in: int, tokens_out: int, git: dict) -> None:
|
|
393
|
+
console.print()
|
|
394
|
+
if executor.files_changed:
|
|
395
|
+
console.print("[bold]Files changed:[/bold]")
|
|
396
|
+
for path in executor.files_changed:
|
|
397
|
+
console.print(f" • {path}")
|
|
398
|
+
if git["repo"]:
|
|
399
|
+
console.print("\n[dim]Review with: git diff · Undo with: git checkout -- .[/dim]")
|
|
400
|
+
else:
|
|
401
|
+
console.print("[dim]No files were modified.[/dim]")
|
|
402
|
+
if tokens_in or tokens_out:
|
|
403
|
+
console.print(f"[dim]{tokens_in} in / {tokens_out} out tokens this run[/dim]")
|