vtx-coding-agent 0.1.1__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.
- vtx/__init__.py +63 -0
- vtx/async_utils.py +40 -0
- vtx/builtin_skills/github/SKILL.md +139 -0
- vtx/builtin_skills/init/SKILL.md +74 -0
- vtx/builtin_skills/review/SKILL.md +73 -0
- vtx/builtin_skills/skill-builder/SKILL.md +133 -0
- vtx/cli.py +90 -0
- vtx/config.py +741 -0
- vtx/context/__init__.py +15 -0
- vtx/context/_xml.py +8 -0
- vtx/context/agent_mds.py +128 -0
- vtx/context/git.py +64 -0
- vtx/context/loader.py +41 -0
- vtx/context/skills.py +423 -0
- vtx/core/__init__.py +47 -0
- vtx/core/compaction.py +89 -0
- vtx/core/errors.py +17 -0
- vtx/core/handoff.py +51 -0
- vtx/core/scratchpad.py +54 -0
- vtx/core/types.py +197 -0
- vtx/defaults/__init__.py +0 -0
- vtx/defaults/config.yml +53 -0
- vtx/diff_display.py +12 -0
- vtx/events.py +224 -0
- vtx/gh_cli.py +82 -0
- vtx/git_branch.py +90 -0
- vtx/headless.py +127 -0
- vtx/llm/__init__.py +93 -0
- vtx/llm/base.py +217 -0
- vtx/llm/context_length.py +150 -0
- vtx/llm/dynamic_models.py +735 -0
- vtx/llm/model_fetcher.py +279 -0
- vtx/llm/models.py +78 -0
- vtx/llm/oauth/__init__.py +59 -0
- vtx/llm/oauth/copilot.py +358 -0
- vtx/llm/oauth/dynamic.py +236 -0
- vtx/llm/oauth/openai.py +400 -0
- vtx/llm/phase_parser.py +270 -0
- vtx/llm/provider.yaml +280 -0
- vtx/llm/provider_catalog.py +230 -0
- vtx/llm/providers/__init__.py +45 -0
- vtx/llm/providers/anthropic_sdk.py +256 -0
- vtx/llm/providers/mock.py +249 -0
- vtx/llm/providers/openai_sdk.py +246 -0
- vtx/llm/providers/sanitize.py +14 -0
- vtx/llm/sdk/__init__.py +13 -0
- vtx/llm/sdk/anthropic.py +382 -0
- vtx/llm/sdk/base.py +82 -0
- vtx/llm/sdk/openai.py +344 -0
- vtx/llm/tool_parser.py +161 -0
- vtx/loop.py +272 -0
- vtx/notify.py +109 -0
- vtx/permissions.py +114 -0
- vtx/prompts/__init__.py +45 -0
- vtx/prompts/builder.py +86 -0
- vtx/prompts/env.py +58 -0
- vtx/prompts/identity.py +166 -0
- vtx/prompts/tooling.py +36 -0
- vtx/py.typed +0 -0
- vtx/runtime.py +580 -0
- vtx/session.py +868 -0
- vtx/sounds/completion.wav +0 -0
- vtx/sounds/error.wav +0 -0
- vtx/sounds/permission.wav +0 -0
- vtx/themes.py +1104 -0
- vtx/tools/__init__.py +68 -0
- vtx/tools/_read_image.py +106 -0
- vtx/tools/_tool_utils.py +90 -0
- vtx/tools/base.py +36 -0
- vtx/tools/bash.py +371 -0
- vtx/tools/edit.py +261 -0
- vtx/tools/find.py +132 -0
- vtx/tools/read.py +238 -0
- vtx/tools/skill.py +278 -0
- vtx/tools/web.py +238 -0
- vtx/tools/write.py +88 -0
- vtx/tools_manager.py +216 -0
- vtx/turn.py +789 -0
- vtx/ui/__init__.py +0 -0
- vtx/ui/agent_runner.py +417 -0
- vtx/ui/app.py +665 -0
- vtx/ui/app_protocol.py +29 -0
- vtx/ui/autocomplete.py +440 -0
- vtx/ui/blocks.py +735 -0
- vtx/ui/chat.py +613 -0
- vtx/ui/clipboard.py +59 -0
- vtx/ui/commands/__init__.py +100 -0
- vtx/ui/commands/auth.py +306 -0
- vtx/ui/commands/base.py +122 -0
- vtx/ui/commands/models.py +144 -0
- vtx/ui/commands/sessions.py +388 -0
- vtx/ui/commands/settings.py +286 -0
- vtx/ui/completion_ui.py +313 -0
- vtx/ui/export.py +703 -0
- vtx/ui/floating_list.py +370 -0
- vtx/ui/formatting.py +287 -0
- vtx/ui/input.py +760 -0
- vtx/ui/latex.py +349 -0
- vtx/ui/launch.py +108 -0
- vtx/ui/path_complete.py +228 -0
- vtx/ui/prompt_history.py +102 -0
- vtx/ui/queue_ui.py +141 -0
- vtx/ui/selection_mode.py +18 -0
- vtx/ui/session_ui.py +235 -0
- vtx/ui/startup.py +124 -0
- vtx/ui/styles.py +327 -0
- vtx/ui/tool_output.py +34 -0
- vtx/ui/tree.py +437 -0
- vtx/ui/welcome.py +51 -0
- vtx/ui/widgets.py +558 -0
- vtx/update_check.py +49 -0
- vtx/version.py +22 -0
- vtx_coding_agent-0.1.1.dist-info/METADATA +259 -0
- vtx_coding_agent-0.1.1.dist-info/RECORD +117 -0
- vtx_coding_agent-0.1.1.dist-info/WHEEL +4 -0
- vtx_coding_agent-0.1.1.dist-info/entry_points.txt +2 -0
- vtx_coding_agent-0.1.1.dist-info/licenses/LICENSE +201 -0
vtx/events.py
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import Any, Literal
|
|
4
|
+
|
|
5
|
+
from .core.types import AssistantMessage, FileChanges, StopReason, ToolResultMessage, Usage
|
|
6
|
+
from .permissions import ApprovalResponse
|
|
7
|
+
|
|
8
|
+
# =================================================================================================
|
|
9
|
+
# Agent Lifecycle Events
|
|
10
|
+
# =================================================================================================
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class AgentStartEvent:
|
|
15
|
+
type: Literal["agent_start"] = "agent_start"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class AgentEndEvent:
|
|
20
|
+
type: Literal["agent_end"] = "agent_end"
|
|
21
|
+
stop_reason: StopReason = StopReason.STOP
|
|
22
|
+
total_turns: int = 0
|
|
23
|
+
total_usage: Usage | None = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# =================================================================================================
|
|
27
|
+
# Turn Lifecycle Events
|
|
28
|
+
# =================================================================================================
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class TurnStartEvent:
|
|
33
|
+
type: Literal["turn_start"] = "turn_start"
|
|
34
|
+
turn: int = 0
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class TurnEndEvent:
|
|
39
|
+
type: Literal["turn_end"] = "turn_end"
|
|
40
|
+
turn: int = 0
|
|
41
|
+
assistant_message: AssistantMessage | None = None
|
|
42
|
+
tool_results: list[ToolResultMessage] = field(default_factory=list)
|
|
43
|
+
stop_reason: StopReason = StopReason.STOP
|
|
44
|
+
tool_call_count: int = 0
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# =================================================================================================
|
|
48
|
+
# Content Streaming Events
|
|
49
|
+
# =================================================================================================
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class ThinkingStartEvent:
|
|
54
|
+
type: Literal["thinking_start"] = "thinking_start"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class ThinkingDeltaEvent:
|
|
59
|
+
type: Literal["thinking_delta"] = "thinking_delta"
|
|
60
|
+
delta: str = ""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class ThinkingEndEvent:
|
|
65
|
+
type: Literal["thinking_end"] = "thinking_end"
|
|
66
|
+
thinking: str = ""
|
|
67
|
+
signature: str | None = None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class TextStartEvent:
|
|
72
|
+
type: Literal["text_start"] = "text_start"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class TextDeltaEvent:
|
|
77
|
+
type: Literal["text_delta"] = "text_delta"
|
|
78
|
+
delta: str = ""
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class TextEndEvent:
|
|
83
|
+
type: Literal["text_end"] = "text_end"
|
|
84
|
+
text: str = ""
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# =================================================================================================
|
|
88
|
+
# Tool Events
|
|
89
|
+
# =================================================================================================
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass
|
|
93
|
+
class ToolStartEvent:
|
|
94
|
+
type: Literal["tool_start"] = "tool_start"
|
|
95
|
+
tool_call_id: str = ""
|
|
96
|
+
tool_name: str = ""
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass
|
|
100
|
+
class ToolArgsDeltaEvent:
|
|
101
|
+
type: Literal["tool_args_delta"] = "tool_args_delta"
|
|
102
|
+
tool_call_id: str = ""
|
|
103
|
+
delta: str = ""
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass
|
|
107
|
+
class ToolArgsTokenUpdateEvent:
|
|
108
|
+
type: Literal["tool_args_token_update"] = "tool_args_token_update"
|
|
109
|
+
tool_call_id: str = ""
|
|
110
|
+
tool_name: str = ""
|
|
111
|
+
token_count: int = 0
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass
|
|
115
|
+
class ToolEndEvent:
|
|
116
|
+
type: Literal["tool_end"] = "tool_end"
|
|
117
|
+
tool_call_id: str = ""
|
|
118
|
+
tool_name: str = ""
|
|
119
|
+
arguments: dict[str, Any] = field(default_factory=dict)
|
|
120
|
+
display: str = "" # Formatted display string from tool.format_call()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass
|
|
124
|
+
class ToolResultEvent:
|
|
125
|
+
type: Literal["tool_result"] = "tool_result"
|
|
126
|
+
tool_call_id: str = ""
|
|
127
|
+
tool_name: str = ""
|
|
128
|
+
result: ToolResultMessage | None = None
|
|
129
|
+
file_changes: FileChanges | None = None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@dataclass
|
|
133
|
+
class ToolApprovalEvent:
|
|
134
|
+
type: Literal["tool_approval"] = "tool_approval"
|
|
135
|
+
tool_call_id: str = ""
|
|
136
|
+
tool_name: str = ""
|
|
137
|
+
display: str = ""
|
|
138
|
+
future: asyncio.Future[ApprovalResponse] | None = None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# =================================================================================================
|
|
142
|
+
# Compaction Events
|
|
143
|
+
# =================================================================================================
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@dataclass
|
|
147
|
+
class CompactionStartEvent:
|
|
148
|
+
type: Literal["compaction_start"] = "compaction_start"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@dataclass
|
|
152
|
+
class CompactionEndEvent:
|
|
153
|
+
type: Literal["compaction_end"] = "compaction_end"
|
|
154
|
+
tokens_before: int = 0
|
|
155
|
+
aborted: bool = False
|
|
156
|
+
reason: str = "" # why compaction aborted, empty on success
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# =================================================================================================
|
|
160
|
+
# Other Events
|
|
161
|
+
# =================================================================================================
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@dataclass
|
|
165
|
+
class RetryEvent:
|
|
166
|
+
type: Literal["retry"] = "retry"
|
|
167
|
+
attempt: int = 0
|
|
168
|
+
total_attempts: int = 3
|
|
169
|
+
delay: float = 0.0
|
|
170
|
+
error: str = ""
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@dataclass
|
|
174
|
+
class ErrorEvent:
|
|
175
|
+
type: Literal["error"] = "error"
|
|
176
|
+
error: str = ""
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@dataclass
|
|
180
|
+
class WarningEvent:
|
|
181
|
+
type: Literal["warning"] = "warning"
|
|
182
|
+
warning: str = ""
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@dataclass
|
|
186
|
+
class InterruptedEvent:
|
|
187
|
+
type: Literal["interrupted"] = "interrupted"
|
|
188
|
+
message: str = "Interrupted by user"
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# =================================================================================================
|
|
192
|
+
# Union Types
|
|
193
|
+
# =================================================================================================
|
|
194
|
+
|
|
195
|
+
# Events yielded by run_single_turn (turn.py)
|
|
196
|
+
StreamEvent = (
|
|
197
|
+
ThinkingStartEvent
|
|
198
|
+
| ThinkingDeltaEvent
|
|
199
|
+
| ThinkingEndEvent
|
|
200
|
+
| TextStartEvent
|
|
201
|
+
| TextDeltaEvent
|
|
202
|
+
| TextEndEvent
|
|
203
|
+
| ToolStartEvent
|
|
204
|
+
| ToolArgsDeltaEvent
|
|
205
|
+
| ToolArgsTokenUpdateEvent
|
|
206
|
+
| ToolEndEvent
|
|
207
|
+
| ToolResultEvent
|
|
208
|
+
| ToolApprovalEvent
|
|
209
|
+
| RetryEvent
|
|
210
|
+
| TurnEndEvent
|
|
211
|
+
| ErrorEvent
|
|
212
|
+
| WarningEvent
|
|
213
|
+
| InterruptedEvent
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# All events yielded by Agent.run() (loop.py)
|
|
217
|
+
Event = (
|
|
218
|
+
AgentStartEvent
|
|
219
|
+
| AgentEndEvent
|
|
220
|
+
| TurnStartEvent
|
|
221
|
+
| CompactionStartEvent
|
|
222
|
+
| CompactionEndEvent
|
|
223
|
+
| StreamEvent
|
|
224
|
+
)
|
vtx/gh_cli.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
import time
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from vtx import AVAILABLE_BINARIES
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class PullRequest:
|
|
13
|
+
number: int
|
|
14
|
+
branch: str
|
|
15
|
+
title: str
|
|
16
|
+
|
|
17
|
+
def chat_reference(self) -> str:
|
|
18
|
+
description = _single_line_description(self.title)
|
|
19
|
+
return f'PR#{self.number} {self.branch} "{description}"'
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _single_line_description(description: str) -> str:
|
|
23
|
+
lines = description.splitlines()
|
|
24
|
+
first_line = lines[0].strip() if lines else ""
|
|
25
|
+
hidden_count = len(lines) - 1
|
|
26
|
+
if hidden_count <= 0:
|
|
27
|
+
return first_line
|
|
28
|
+
return f"{first_line} ... ({hidden_count} lines hidden)"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_CACHE_TTL_SECONDS = 30.0
|
|
32
|
+
_cached_cwd: str | None = None
|
|
33
|
+
_cached_at: float = 0.0
|
|
34
|
+
_cached_prs: list[PullRequest] = []
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def is_available() -> bool:
|
|
38
|
+
return "gh" in AVAILABLE_BINARIES
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def list_pull_requests(cwd: str = ".") -> list[PullRequest]:
|
|
42
|
+
global _cached_at, _cached_cwd, _cached_prs
|
|
43
|
+
|
|
44
|
+
if not is_available():
|
|
45
|
+
return []
|
|
46
|
+
|
|
47
|
+
now = time.monotonic()
|
|
48
|
+
if _cached_cwd == cwd and now - _cached_at < _CACHE_TTL_SECONDS:
|
|
49
|
+
return _cached_prs
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
result = subprocess.run(
|
|
53
|
+
["gh", "pr", "list", "--json", "number,headRefName,title", "--limit", "50"],
|
|
54
|
+
cwd=cwd,
|
|
55
|
+
capture_output=True,
|
|
56
|
+
text=True,
|
|
57
|
+
timeout=2.0,
|
|
58
|
+
)
|
|
59
|
+
except Exception:
|
|
60
|
+
return []
|
|
61
|
+
|
|
62
|
+
if result.returncode != 0:
|
|
63
|
+
return []
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
raw_prs = json.loads(result.stdout)
|
|
67
|
+
except json.JSONDecodeError:
|
|
68
|
+
return []
|
|
69
|
+
|
|
70
|
+
prs = [
|
|
71
|
+
PullRequest(
|
|
72
|
+
number=int(pr["number"]),
|
|
73
|
+
branch=str(pr.get("headRefName") or ""),
|
|
74
|
+
title=str(pr.get("title") or ""),
|
|
75
|
+
)
|
|
76
|
+
for pr in raw_prs
|
|
77
|
+
if "number" in pr
|
|
78
|
+
]
|
|
79
|
+
_cached_cwd = cwd
|
|
80
|
+
_cached_at = now
|
|
81
|
+
_cached_prs = prs
|
|
82
|
+
return prs
|
vtx/git_branch.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True)
|
|
7
|
+
class GitPaths:
|
|
8
|
+
repo_dir: str
|
|
9
|
+
common_git_dir: str
|
|
10
|
+
head_path: str
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def find_git_paths(cwd: str) -> GitPaths | None:
|
|
14
|
+
"""Find git metadata paths for regular repos and worktrees."""
|
|
15
|
+
directory = os.path.abspath(cwd)
|
|
16
|
+
while True:
|
|
17
|
+
git_path = os.path.join(directory, ".git")
|
|
18
|
+
if os.path.exists(git_path):
|
|
19
|
+
try:
|
|
20
|
+
if os.path.isfile(git_path):
|
|
21
|
+
with open(git_path, encoding="utf-8") as f:
|
|
22
|
+
content = f.read().strip()
|
|
23
|
+
if content.startswith("gitdir: "):
|
|
24
|
+
git_dir = os.path.abspath(
|
|
25
|
+
os.path.join(directory, content.removeprefix("gitdir: ").strip())
|
|
26
|
+
)
|
|
27
|
+
head_path = os.path.join(git_dir, "HEAD")
|
|
28
|
+
if not os.path.exists(head_path):
|
|
29
|
+
return None
|
|
30
|
+
common_dir_path = os.path.join(git_dir, "commondir")
|
|
31
|
+
if os.path.exists(common_dir_path):
|
|
32
|
+
with open(common_dir_path, encoding="utf-8") as f:
|
|
33
|
+
common_dir = f.read().strip()
|
|
34
|
+
common_git_dir = os.path.abspath(os.path.join(git_dir, common_dir))
|
|
35
|
+
else:
|
|
36
|
+
common_git_dir = git_dir
|
|
37
|
+
return GitPaths(directory, common_git_dir, head_path)
|
|
38
|
+
elif os.path.isdir(git_path):
|
|
39
|
+
head_path = os.path.join(git_path, "HEAD")
|
|
40
|
+
if not os.path.exists(head_path):
|
|
41
|
+
return None
|
|
42
|
+
return GitPaths(directory, git_path, head_path)
|
|
43
|
+
except OSError:
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
parent = os.path.dirname(directory)
|
|
47
|
+
if parent == directory:
|
|
48
|
+
return None
|
|
49
|
+
directory = parent
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _resolve_branch_with_git(repo_dir: str) -> str | None:
|
|
53
|
+
try:
|
|
54
|
+
result = subprocess.run(
|
|
55
|
+
["git", "--no-optional-locks", "symbolic-ref", "--quiet", "--short", "HEAD"],
|
|
56
|
+
cwd=repo_dir,
|
|
57
|
+
capture_output=True,
|
|
58
|
+
text=True,
|
|
59
|
+
timeout=1,
|
|
60
|
+
check=False,
|
|
61
|
+
)
|
|
62
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
if result.returncode != 0:
|
|
66
|
+
return None
|
|
67
|
+
branch = result.stdout.strip()
|
|
68
|
+
return branch or None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def resolve_git_branch(cwd: str) -> str:
|
|
72
|
+
"""Resolve current git branch, returning an empty string outside git repos."""
|
|
73
|
+
git_paths = find_git_paths(cwd)
|
|
74
|
+
if git_paths is None:
|
|
75
|
+
return ""
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
with open(git_paths.head_path, encoding="utf-8") as f:
|
|
79
|
+
content = f.read().strip()
|
|
80
|
+
except OSError:
|
|
81
|
+
return ""
|
|
82
|
+
|
|
83
|
+
prefix = "ref: refs/heads/"
|
|
84
|
+
if content.startswith(prefix):
|
|
85
|
+
branch = content.removeprefix(prefix)
|
|
86
|
+
if branch == ".invalid":
|
|
87
|
+
return _resolve_branch_with_git(git_paths.repo_dir) or "detached"
|
|
88
|
+
return branch
|
|
89
|
+
|
|
90
|
+
return "detached"
|
vtx/headless.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
from collections.abc import AsyncIterator
|
|
4
|
+
from typing import TextIO
|
|
5
|
+
|
|
6
|
+
from vtx import config, get_config
|
|
7
|
+
from vtx.config import get_last_selected
|
|
8
|
+
|
|
9
|
+
from .core.types import StopReason, TextContent
|
|
10
|
+
from .events import AgentEndEvent, ErrorEvent, Event, ToolApprovalEvent, TurnEndEvent
|
|
11
|
+
from .llm.base import AuthMode
|
|
12
|
+
from .permissions import ApprovalResponse
|
|
13
|
+
from .runtime import ConversationRuntime
|
|
14
|
+
from .tools import DEFAULT_TOOLS, get_tools
|
|
15
|
+
|
|
16
|
+
_EXIT_CODES = {StopReason.STOP: 0, StopReason.ERROR: 1, StopReason.LENGTH: 3}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _exit_code(stop: StopReason) -> int:
|
|
20
|
+
return _EXIT_CODES.get(stop, 1)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def resolve_prompt(prompt_arg: str, *, stdin: TextIO) -> str:
|
|
24
|
+
if prompt_arg == "-":
|
|
25
|
+
return stdin.read().strip()
|
|
26
|
+
return prompt_arg.strip()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def render_run(
|
|
30
|
+
events: AsyncIterator[Event], *, out: TextIO | None = None, err: TextIO | None = None
|
|
31
|
+
) -> StopReason:
|
|
32
|
+
out = sys.stdout if out is None else out
|
|
33
|
+
err = sys.stderr if err is None else err
|
|
34
|
+
final_text = ""
|
|
35
|
+
stop = StopReason.ERROR
|
|
36
|
+
async for event in events:
|
|
37
|
+
match event:
|
|
38
|
+
case TurnEndEvent(assistant_message=msg) if msg is not None:
|
|
39
|
+
text = "".join(p.text for p in msg.content if isinstance(p, TextContent)).strip()
|
|
40
|
+
if text:
|
|
41
|
+
final_text = text
|
|
42
|
+
case AgentEndEvent(stop_reason=stop_reason):
|
|
43
|
+
stop = stop_reason
|
|
44
|
+
case ErrorEvent(error=error):
|
|
45
|
+
print(f"error: {error}", file=err)
|
|
46
|
+
case ToolApprovalEvent(tool_name=tool_name, future=future) if future is not None:
|
|
47
|
+
future.set_result(ApprovalResponse.DENY)
|
|
48
|
+
print(
|
|
49
|
+
f"error: {tool_name!r} requires approval, denied (non-interactive mode)",
|
|
50
|
+
file=err,
|
|
51
|
+
)
|
|
52
|
+
case _:
|
|
53
|
+
pass
|
|
54
|
+
if stop == StopReason.STOP and final_text:
|
|
55
|
+
print(final_text, file=out)
|
|
56
|
+
return stop
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def run_headless(
|
|
60
|
+
*,
|
|
61
|
+
prompt_arg: str,
|
|
62
|
+
model: str | None,
|
|
63
|
+
provider: str | None,
|
|
64
|
+
api_key: str | None,
|
|
65
|
+
base_url: str | None,
|
|
66
|
+
openai_compat_auth_mode: AuthMode | None,
|
|
67
|
+
anthropic_compat_auth_mode: AuthMode | None,
|
|
68
|
+
) -> int:
|
|
69
|
+
prompt = resolve_prompt(prompt_arg, stdin=sys.stdin)
|
|
70
|
+
if not prompt:
|
|
71
|
+
print("error: empty prompt", file=sys.stderr)
|
|
72
|
+
return 2
|
|
73
|
+
|
|
74
|
+
cfg = get_config()
|
|
75
|
+
previous_permission_mode = cfg.permissions.mode
|
|
76
|
+
# Headless can't show approval prompts; force auto in-memory for this run only.
|
|
77
|
+
cfg.permissions.mode = "auto"
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
last_selected = get_last_selected()
|
|
81
|
+
initial_model = model or last_selected.model_id or config.llm.default_model
|
|
82
|
+
initial_provider = (
|
|
83
|
+
provider
|
|
84
|
+
if provider is not None
|
|
85
|
+
else (
|
|
86
|
+
last_selected.provider
|
|
87
|
+
if last_selected.model_id
|
|
88
|
+
else (config.llm.default_provider if model is None else None)
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
base = base_url or config.llm.default_base_url or None
|
|
92
|
+
thinking = last_selected.thinking_level or config.llm.default_thinking_level
|
|
93
|
+
openai_auth = openai_compat_auth_mode or config.llm.auth.openai_compat
|
|
94
|
+
anthropic_auth = anthropic_compat_auth_mode or config.llm.auth.anthropic_compat
|
|
95
|
+
|
|
96
|
+
tools = get_tools(DEFAULT_TOOLS)
|
|
97
|
+
|
|
98
|
+
runtime = ConversationRuntime(
|
|
99
|
+
cwd=os.getcwd(),
|
|
100
|
+
model=initial_model,
|
|
101
|
+
model_provider=initial_provider,
|
|
102
|
+
api_key=api_key,
|
|
103
|
+
base_url=base,
|
|
104
|
+
thinking_level=thinking,
|
|
105
|
+
tools=tools,
|
|
106
|
+
openai_compat_auth_mode=openai_auth,
|
|
107
|
+
anthropic_compat_auth_mode=anthropic_auth,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
init = runtime.initialize()
|
|
112
|
+
if init.provider_error:
|
|
113
|
+
print(f"error: {init.provider_error}", file=sys.stderr)
|
|
114
|
+
return 2
|
|
115
|
+
|
|
116
|
+
agent = runtime.prepare_for_run()
|
|
117
|
+
except Exception as e:
|
|
118
|
+
print(f"error: {e}", file=sys.stderr)
|
|
119
|
+
return 2
|
|
120
|
+
|
|
121
|
+
if agent is None:
|
|
122
|
+
print("error: agent initialization failed", file=sys.stderr)
|
|
123
|
+
return 2
|
|
124
|
+
|
|
125
|
+
return _exit_code(await render_run(agent.run(prompt)))
|
|
126
|
+
finally:
|
|
127
|
+
cfg.permissions.mode = previous_permission_mode
|
vtx/llm/__init__.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from .base import DEFAULT_THINKING_LEVELS, BaseProvider, LLMStream, ProviderConfig
|
|
2
|
+
from .dynamic_models import (
|
|
3
|
+
DYNAMIC_PROVIDERS,
|
|
4
|
+
DynamicModelEntry,
|
|
5
|
+
DynamicProviderConfig,
|
|
6
|
+
find_dynamic_model,
|
|
7
|
+
get_all_models_with_dynamic,
|
|
8
|
+
get_dynamic_models,
|
|
9
|
+
get_dynamic_provider,
|
|
10
|
+
get_dynamic_provider_headers,
|
|
11
|
+
get_provider_models,
|
|
12
|
+
refresh_all_providers,
|
|
13
|
+
refresh_provider,
|
|
14
|
+
register_dynamic_provider,
|
|
15
|
+
)
|
|
16
|
+
from .models import (
|
|
17
|
+
ApiType,
|
|
18
|
+
Model,
|
|
19
|
+
get_all_models,
|
|
20
|
+
get_max_tokens,
|
|
21
|
+
get_model,
|
|
22
|
+
get_models_by_provider,
|
|
23
|
+
)
|
|
24
|
+
from .oauth import (
|
|
25
|
+
clear_api_key,
|
|
26
|
+
clear_openai_credentials,
|
|
27
|
+
get_dynamic_api_key,
|
|
28
|
+
get_provider_status,
|
|
29
|
+
get_valid_openai_credentials,
|
|
30
|
+
has_api_key,
|
|
31
|
+
is_copilot_logged_in,
|
|
32
|
+
is_openai_logged_in,
|
|
33
|
+
load_api_key,
|
|
34
|
+
load_openai_credentials,
|
|
35
|
+
openai_login,
|
|
36
|
+
save_api_key,
|
|
37
|
+
)
|
|
38
|
+
from .oauth import clear_credentials as clear_copilot_credentials
|
|
39
|
+
from .oauth import get_valid_token as get_copilot_token
|
|
40
|
+
from .oauth import load_credentials as load_copilot_credentials
|
|
41
|
+
from .oauth import login as copilot_login
|
|
42
|
+
from .provider_catalog import ProviderInfo, detect_provider_from_env, list_providers
|
|
43
|
+
from .provider_catalog import get as get_provider_info
|
|
44
|
+
from .providers import PROVIDER_API_BY_NAME, get_provider_class, resolve_provider_api_type
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
"DEFAULT_THINKING_LEVELS",
|
|
48
|
+
"DYNAMIC_PROVIDERS",
|
|
49
|
+
"PROVIDER_API_BY_NAME",
|
|
50
|
+
"ApiType",
|
|
51
|
+
"BaseProvider",
|
|
52
|
+
"DynamicModelEntry",
|
|
53
|
+
"DynamicProviderConfig",
|
|
54
|
+
"LLMStream",
|
|
55
|
+
"Model",
|
|
56
|
+
"ProviderConfig",
|
|
57
|
+
"ProviderInfo",
|
|
58
|
+
"clear_api_key",
|
|
59
|
+
"clear_copilot_credentials",
|
|
60
|
+
"clear_openai_credentials",
|
|
61
|
+
"copilot_login",
|
|
62
|
+
"detect_provider_from_env",
|
|
63
|
+
"find_dynamic_model",
|
|
64
|
+
"get_all_models",
|
|
65
|
+
"get_all_models_with_dynamic",
|
|
66
|
+
"get_copilot_token",
|
|
67
|
+
"get_dynamic_api_key",
|
|
68
|
+
"get_dynamic_models",
|
|
69
|
+
"get_dynamic_provider",
|
|
70
|
+
"get_dynamic_provider_headers",
|
|
71
|
+
"get_max_tokens",
|
|
72
|
+
"get_model",
|
|
73
|
+
"get_models_by_provider",
|
|
74
|
+
"get_openai_token",
|
|
75
|
+
"get_provider_class",
|
|
76
|
+
"get_provider_info",
|
|
77
|
+
"get_provider_models",
|
|
78
|
+
"get_provider_status",
|
|
79
|
+
"get_valid_openai_credentials",
|
|
80
|
+
"has_api_key",
|
|
81
|
+
"is_copilot_logged_in",
|
|
82
|
+
"is_openai_logged_in",
|
|
83
|
+
"list_providers",
|
|
84
|
+
"load_api_key",
|
|
85
|
+
"load_copilot_credentials",
|
|
86
|
+
"load_openai_credentials",
|
|
87
|
+
"openai_login",
|
|
88
|
+
"refresh_all_providers",
|
|
89
|
+
"refresh_provider",
|
|
90
|
+
"register_dynamic_provider",
|
|
91
|
+
"resolve_provider_api_type",
|
|
92
|
+
"save_api_key",
|
|
93
|
+
]
|