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/core/__init__.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from .errors import format_error
|
|
2
|
+
from .scratchpad import get_scratchpad_dir, init_scratchpad, is_scratchpad_path
|
|
3
|
+
from .types import (
|
|
4
|
+
AssistantMessage,
|
|
5
|
+
ImageContent,
|
|
6
|
+
Message,
|
|
7
|
+
StopReason,
|
|
8
|
+
StreamDone,
|
|
9
|
+
StreamError,
|
|
10
|
+
StreamPart,
|
|
11
|
+
TextContent,
|
|
12
|
+
TextPart,
|
|
13
|
+
ThinkingContent,
|
|
14
|
+
ThinkPart,
|
|
15
|
+
ToolCall,
|
|
16
|
+
ToolCallDelta,
|
|
17
|
+
ToolCallStart,
|
|
18
|
+
ToolDefinition,
|
|
19
|
+
ToolResultMessage,
|
|
20
|
+
Usage,
|
|
21
|
+
UserMessage,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"AssistantMessage",
|
|
26
|
+
"ImageContent",
|
|
27
|
+
"Message",
|
|
28
|
+
"StopReason",
|
|
29
|
+
"StreamDone",
|
|
30
|
+
"StreamError",
|
|
31
|
+
"StreamPart",
|
|
32
|
+
"TextContent",
|
|
33
|
+
"TextPart",
|
|
34
|
+
"ThinkPart",
|
|
35
|
+
"ThinkingContent",
|
|
36
|
+
"ToolCall",
|
|
37
|
+
"ToolCallDelta",
|
|
38
|
+
"ToolCallStart",
|
|
39
|
+
"ToolDefinition",
|
|
40
|
+
"ToolResultMessage",
|
|
41
|
+
"Usage",
|
|
42
|
+
"UserMessage",
|
|
43
|
+
"format_error",
|
|
44
|
+
"get_scratchpad_dir",
|
|
45
|
+
"init_scratchpad",
|
|
46
|
+
"is_scratchpad_path",
|
|
47
|
+
]
|
vtx/core/compaction.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Context compaction for long sessions.
|
|
3
|
+
|
|
4
|
+
When token usage exceeds the usable context window, send the full conversation
|
|
5
|
+
to the LLM with a summarization prompt, then store the summary as a
|
|
6
|
+
CompactionEntry. The session.messages property filters to only show messages
|
|
7
|
+
after the compaction point.
|
|
8
|
+
|
|
9
|
+
Overflow formula:
|
|
10
|
+
total_tokens >= context_window - min(buffer_tokens, max_output_tokens)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from ..core.types import Message, TextPart, Usage, UserMessage
|
|
14
|
+
from ..llm.base import BaseProvider
|
|
15
|
+
|
|
16
|
+
SUMMARIZATION_PROMPT = """Provide a detailed prompt for continuing our \
|
|
17
|
+
conversation above. Focus on information that would be helpful for \
|
|
18
|
+
continuing the conversation, including what we did, what we're doing, \
|
|
19
|
+
which files we're working on, and what we're going to do next. \
|
|
20
|
+
The summary that you construct will be used so that another agent \
|
|
21
|
+
can read it and continue the work.
|
|
22
|
+
|
|
23
|
+
When constructing the summary, try to stick to this template:
|
|
24
|
+
---
|
|
25
|
+
## Goal
|
|
26
|
+
|
|
27
|
+
[What goal(s) is the user trying to accomplish?]
|
|
28
|
+
|
|
29
|
+
## Instructions
|
|
30
|
+
|
|
31
|
+
- [What important instructions did the user give you that are relevant]
|
|
32
|
+
- [If there is a plan or spec, include information about it
|
|
33
|
+
so next agent can continue using it]
|
|
34
|
+
|
|
35
|
+
## Discoveries
|
|
36
|
+
|
|
37
|
+
[What notable things were learned during this conversation that would
|
|
38
|
+
be useful for the next agent to know when continuing the work]
|
|
39
|
+
|
|
40
|
+
## Accomplished
|
|
41
|
+
|
|
42
|
+
[What work has been completed, what work is still in progress,
|
|
43
|
+
and what work is left?]
|
|
44
|
+
|
|
45
|
+
## Relevant files / directories
|
|
46
|
+
|
|
47
|
+
[Construct a structured list of relevant files that have been read,
|
|
48
|
+
edited, or created that pertain to the task at hand. If all the files
|
|
49
|
+
in a directory are relevant, include the path to the directory.]
|
|
50
|
+
---"""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def is_overflow(
|
|
54
|
+
usage: Usage, context_window: int, max_output_tokens: int, buffer_tokens: int
|
|
55
|
+
) -> bool:
|
|
56
|
+
count = (
|
|
57
|
+
usage.input_tokens
|
|
58
|
+
+ usage.output_tokens
|
|
59
|
+
+ usage.cache_read_tokens
|
|
60
|
+
+ usage.cache_write_tokens
|
|
61
|
+
)
|
|
62
|
+
reserved = min(buffer_tokens, max_output_tokens)
|
|
63
|
+
usable = context_window - reserved
|
|
64
|
+
return count >= usable
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _calculate_context_tokens(usage: Usage) -> int:
|
|
68
|
+
return (
|
|
69
|
+
usage.input_tokens
|
|
70
|
+
+ usage.output_tokens
|
|
71
|
+
+ usage.cache_read_tokens
|
|
72
|
+
+ usage.cache_write_tokens
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
async def generate_summary(
|
|
77
|
+
messages: list[Message], provider: BaseProvider, system_prompt: str | None = None
|
|
78
|
+
) -> str:
|
|
79
|
+
"""Send the full conversation + summarization prompt to the LLM, return summary text."""
|
|
80
|
+
summary_messages: list[Message] = [*messages, UserMessage(content=SUMMARIZATION_PROMPT)]
|
|
81
|
+
|
|
82
|
+
stream = await provider.stream(summary_messages, system_prompt=system_prompt, tools=None)
|
|
83
|
+
|
|
84
|
+
text_parts: list[str] = []
|
|
85
|
+
async for part in stream:
|
|
86
|
+
if isinstance(part, TextPart):
|
|
87
|
+
text_parts.append(part.text)
|
|
88
|
+
|
|
89
|
+
return "".join(text_parts)
|
vtx/core/errors.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Shared error formatting for provider/agent error surfaces.
|
|
2
|
+
|
|
3
|
+
Errors crossing the event boundary (StreamError, ErrorEvent, compaction
|
|
4
|
+
failures) are reduced to strings, so the exception type must be baked into
|
|
5
|
+
the message or it is lost to the user.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def format_error(error: BaseException) -> str:
|
|
10
|
+
name = type(error).__name__
|
|
11
|
+
message = str(error).strip()
|
|
12
|
+
if not message:
|
|
13
|
+
return f"{name}: failed without an error message"
|
|
14
|
+
# Some SDK errors already stringify with their type name; don't repeat it.
|
|
15
|
+
if message.startswith(name):
|
|
16
|
+
return message
|
|
17
|
+
return f"{name}: {message}"
|
vtx/core/handoff.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from ..core.types import Message, TextPart, UserMessage
|
|
2
|
+
from ..llm.base import BaseProvider
|
|
3
|
+
|
|
4
|
+
HANDOFF_PROMPT_TEMPLATE = """You are creating a handoff to a NEW focused thread.
|
|
5
|
+
|
|
6
|
+
New thread goal (from user):
|
|
7
|
+
{query}
|
|
8
|
+
|
|
9
|
+
Based on the conversation above, write the exact opening user prompt for the new thread.
|
|
10
|
+
|
|
11
|
+
Requirements:
|
|
12
|
+
- Focus ONLY on context relevant to the new goal.
|
|
13
|
+
- Preserve critical decisions, constraints, and assumptions.
|
|
14
|
+
- Include concrete file paths and why they matter (only if relevant).
|
|
15
|
+
- Include current status: done, in progress, and next action.
|
|
16
|
+
- Do not invent facts; if unknown, say "Unknown".
|
|
17
|
+
- Do not include backlinks, UI notes, or any metadata.
|
|
18
|
+
- Do not mention "handoff", "summary", or "conversation above".
|
|
19
|
+
- Output must be ready to send as-is by the user in the new thread.
|
|
20
|
+
|
|
21
|
+
Output format (plain text, no markdown code fences):
|
|
22
|
+
Task: <clear goal>
|
|
23
|
+
|
|
24
|
+
Context to keep:
|
|
25
|
+
- ...
|
|
26
|
+
|
|
27
|
+
Relevant files:
|
|
28
|
+
- <path> — <why it matters>
|
|
29
|
+
|
|
30
|
+
Constraints:
|
|
31
|
+
- ...
|
|
32
|
+
|
|
33
|
+
Next steps:
|
|
34
|
+
1. ...
|
|
35
|
+
2. ..."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
async def generate_handoff_prompt(
|
|
39
|
+
messages: list[Message], provider: BaseProvider, system_prompt: str | None, query: str
|
|
40
|
+
) -> str:
|
|
41
|
+
handoff_prompt = HANDOFF_PROMPT_TEMPLATE.format(query=query.strip())
|
|
42
|
+
handoff_messages: list[Message] = [*messages, UserMessage(content=handoff_prompt)]
|
|
43
|
+
|
|
44
|
+
stream = await provider.stream(handoff_messages, system_prompt=system_prompt, tools=None)
|
|
45
|
+
|
|
46
|
+
text_parts: list[str] = []
|
|
47
|
+
async for part in stream:
|
|
48
|
+
if isinstance(part, TextPart):
|
|
49
|
+
text_parts.append(part.text)
|
|
50
|
+
|
|
51
|
+
return "".join(text_parts).strip()
|
vtx/core/scratchpad.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from vtx import get_config_dir
|
|
6
|
+
|
|
7
|
+
_active_scratchpads: dict[str, Path] = {}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def init_scratchpad(session_id: str) -> Path | None:
|
|
11
|
+
"""Create a session-scoped scratchpad directory under VTX config dir.
|
|
12
|
+
|
|
13
|
+
Each session gets its own scratchpad directory. Idempotent per session_id.
|
|
14
|
+
"""
|
|
15
|
+
if session_id in _active_scratchpads:
|
|
16
|
+
return _active_scratchpads[session_id]
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
config_dir = get_config_dir()
|
|
20
|
+
dir_path = config_dir / "scratchpads" / f"vtx-scratchpad-{session_id[:8]}"
|
|
21
|
+
dir_path.mkdir(parents=True, exist_ok=True)
|
|
22
|
+
_active_scratchpads[session_id] = dir_path
|
|
23
|
+
return dir_path
|
|
24
|
+
except OSError:
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_scratchpad_dir(session_id: str) -> Path | None:
|
|
29
|
+
"""Return the scratchpad directory for a given session, or None."""
|
|
30
|
+
return _active_scratchpads.get(session_id)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def is_scratchpad_path(path_str: str) -> bool:
|
|
34
|
+
"""Return True if the resolved path is inside any active scratchpad.
|
|
35
|
+
|
|
36
|
+
Uses Path.resolve() to defeat path traversal and symlink attacks.
|
|
37
|
+
"""
|
|
38
|
+
if not _active_scratchpads:
|
|
39
|
+
return False
|
|
40
|
+
try:
|
|
41
|
+
resolved = Path(path_str).expanduser().resolve()
|
|
42
|
+
return any(
|
|
43
|
+
_is_subpath(resolved, sp_dir.resolve()) for sp_dir in _active_scratchpads.values()
|
|
44
|
+
)
|
|
45
|
+
except (ValueError, OSError):
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _is_subpath(path: Path, parent: Path) -> bool:
|
|
50
|
+
try:
|
|
51
|
+
path.relative_to(parent)
|
|
52
|
+
return True
|
|
53
|
+
except ValueError:
|
|
54
|
+
return False
|
vtx/core/types.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
from typing import Any, Literal
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class StopReason(StrEnum):
|
|
10
|
+
STOP = "stop"
|
|
11
|
+
LENGTH = "length"
|
|
12
|
+
TOOL_USE = "tool_use"
|
|
13
|
+
ERROR = "error"
|
|
14
|
+
INTERRUPTED = "interrupted"
|
|
15
|
+
STEER = "steer"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Usage(BaseModel):
|
|
19
|
+
input_tokens: int = 0
|
|
20
|
+
output_tokens: int = 0
|
|
21
|
+
cache_read_tokens: int = 0
|
|
22
|
+
cache_write_tokens: int = 0
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def total_tokens(self) -> int:
|
|
26
|
+
return (
|
|
27
|
+
self.input_tokens
|
|
28
|
+
+ self.output_tokens
|
|
29
|
+
+ self.cache_read_tokens
|
|
30
|
+
+ self.cache_write_tokens
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# =================================================================================================
|
|
35
|
+
# Stream Parts - yielded by providers during streaming
|
|
36
|
+
# =================================================================================================
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TextPart(BaseModel):
|
|
40
|
+
type: Literal["text"] = "text"
|
|
41
|
+
text: str
|
|
42
|
+
|
|
43
|
+
def merge(self, other: TextPart) -> TextPart:
|
|
44
|
+
return TextPart(text=self.text + other.text)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ThinkPart(BaseModel):
|
|
48
|
+
type: Literal["think"] = "think"
|
|
49
|
+
think: str
|
|
50
|
+
signature: str | None = None
|
|
51
|
+
|
|
52
|
+
def merge(self, other: ThinkPart) -> ThinkPart:
|
|
53
|
+
signature = self.signature or other.signature
|
|
54
|
+
return ThinkPart(think=self.think + other.think, signature=signature)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ToolCallStart(BaseModel):
|
|
58
|
+
type: Literal["tool_call_start"] = "tool_call_start"
|
|
59
|
+
id: str
|
|
60
|
+
name: str
|
|
61
|
+
index: int # Tool call index for correlating deltas
|
|
62
|
+
arguments: dict[str, Any] | None = None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ToolCallDelta(BaseModel):
|
|
66
|
+
type: Literal["tool_call_delta"] = "tool_call_delta"
|
|
67
|
+
index: int # Correlates with ToolCallStart.index
|
|
68
|
+
arguments_delta: str
|
|
69
|
+
replace: bool = False
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class StreamDone(BaseModel):
|
|
73
|
+
type: Literal["done"] = "done"
|
|
74
|
+
stop_reason: StopReason
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class StreamError(BaseModel):
|
|
78
|
+
type: Literal["error"] = "error"
|
|
79
|
+
error: str
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
StreamPart = TextPart | ThinkPart | ToolCallStart | ToolCallDelta | StreamDone | StreamError
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# =================================================================================================
|
|
86
|
+
# Message Types - canonical provider-agnostic conversation interface
|
|
87
|
+
# Used for conversation history and cross-provider normalization
|
|
88
|
+
# =================================================================================================
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class TextContent(BaseModel):
|
|
92
|
+
type: Literal["text"] = "text"
|
|
93
|
+
text: str
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class ThinkingContent(BaseModel):
|
|
97
|
+
type: Literal["thinking"] = "thinking"
|
|
98
|
+
thinking: str
|
|
99
|
+
signature: str | None = None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class ImageContent(BaseModel):
|
|
103
|
+
type: Literal["image"] = "image"
|
|
104
|
+
data: str # base64 encoded
|
|
105
|
+
mime_type: str
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class ToolCall(BaseModel):
|
|
109
|
+
type: Literal["tool_call"] = "tool_call"
|
|
110
|
+
id: str
|
|
111
|
+
name: str
|
|
112
|
+
arguments: dict[str, Any]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class UserMessage(BaseModel):
|
|
116
|
+
role: Literal["user"] = "user"
|
|
117
|
+
content: str | list[TextContent | ImageContent]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class AssistantMessage(BaseModel):
|
|
121
|
+
role: Literal["assistant"] = "assistant"
|
|
122
|
+
content: list[TextContent | ThinkingContent | ToolCall]
|
|
123
|
+
usage: Usage | None = None
|
|
124
|
+
stop_reason: StopReason | None = None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class ToolResultMessage(BaseModel):
|
|
128
|
+
role: Literal["tool_result"] = "tool_result"
|
|
129
|
+
tool_call_id: str
|
|
130
|
+
tool_name: str
|
|
131
|
+
content: list[TextContent | ImageContent]
|
|
132
|
+
ui_summary: str | None = None # One-line UI text rendered on tool header line
|
|
133
|
+
ui_details: str | None = None # Collapsed multiline UI text rendered below the header
|
|
134
|
+
ui_details_full: str | None = None # Expanded multiline UI text rendered below the header
|
|
135
|
+
is_error: bool = False
|
|
136
|
+
file_changes: FileChanges | None = None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
Message = UserMessage | AssistantMessage | ToolResultMessage
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# =================================================================================================
|
|
143
|
+
# Tool Definition
|
|
144
|
+
# =================================================================================================
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class ToolParameter(BaseModel):
|
|
148
|
+
type: str
|
|
149
|
+
description: str | None = None
|
|
150
|
+
enum: list[str] | None = None
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class ToolDefinition(BaseModel):
|
|
154
|
+
name: str
|
|
155
|
+
description: str
|
|
156
|
+
parameters: dict[str, Any] # JSON Schema
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class FileChanges(BaseModel):
|
|
160
|
+
path: str
|
|
161
|
+
added: int = 0
|
|
162
|
+
removed: int = 0
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class ToolResult(BaseModel):
|
|
166
|
+
success: bool
|
|
167
|
+
result: str | None = None # Raw result (sent to LLM)
|
|
168
|
+
images: list[ImageContent] | None = None # Images to include in result
|
|
169
|
+
ui_summary: str | None = None # One-line result text appended to the tool header
|
|
170
|
+
ui_details: str | None = None # Collapsed multiline result body rendered below the header
|
|
171
|
+
ui_details_full: str | None = None # Expanded multiline result body rendered below the header
|
|
172
|
+
file_changes: FileChanges | None = None # Track +/- lines for edit/write tools
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# UI rendering model:
|
|
176
|
+
#
|
|
177
|
+
# format_call is defined for each tool like Read tool and the result they
|
|
178
|
+
# return contains further details (along with the resulf for llm) to help paint
|
|
179
|
+
# the coomplete picture (or as close to it as possible without polluting) in the ui
|
|
180
|
+
#
|
|
181
|
+
# - format_call(params): short call text shown on the header line
|
|
182
|
+
# - ui_summary: one-line result summary appended to the same header line
|
|
183
|
+
# - ui_details: multiline result body shown below the header
|
|
184
|
+
#
|
|
185
|
+
# Example (read):
|
|
186
|
+
# → Read ~/src/vtx/turn.py:150-204 (55 lines)
|
|
187
|
+
# - format_call -> "~/src/vtx/turn.py:150-204"
|
|
188
|
+
# - ui_summary -> "(55 lines)"
|
|
189
|
+
# - ui_details -> None
|
|
190
|
+
#
|
|
191
|
+
# Example (edit):
|
|
192
|
+
# + Edit ~/src/vtx/tools/base.py +3 -1
|
|
193
|
+
# -12 old line
|
|
194
|
+
# +12 new line
|
|
195
|
+
# - format_call -> "~/src/vtx/tools/base.py"
|
|
196
|
+
# - ui_summary -> "+3 -1"
|
|
197
|
+
# - ui_details -> formatted diff
|
vtx/defaults/__init__.py
ADDED
|
File without changes
|
vtx/defaults/config.yml
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
meta:
|
|
2
|
+
config_version: 6
|
|
3
|
+
|
|
4
|
+
llm:
|
|
5
|
+
default_provider: "openai-codex"
|
|
6
|
+
default_model: "gpt-5.5"
|
|
7
|
+
default_base_url: ""
|
|
8
|
+
default_thinking_level: "low"
|
|
9
|
+
tool_call_idle_timeout_seconds: 180
|
|
10
|
+
request_timeout_seconds: 600
|
|
11
|
+
|
|
12
|
+
auth:
|
|
13
|
+
openai_compat: "auto"
|
|
14
|
+
anthropic_compat: "auto"
|
|
15
|
+
|
|
16
|
+
tls:
|
|
17
|
+
insecure_skip_verify: false
|
|
18
|
+
|
|
19
|
+
system_prompt:
|
|
20
|
+
git_context: true
|
|
21
|
+
# Base identity + general rules. Defaults to vtx.prompts.identity.DEFAULT_VTX_BASE
|
|
22
|
+
# (sourced from Python). Set a custom string here to override the base prompt;
|
|
23
|
+
# extra sections (tool guidelines, AGENTS.md, skills, git, env) are still
|
|
24
|
+
# appended automatically.
|
|
25
|
+
content: ""
|
|
26
|
+
|
|
27
|
+
compaction:
|
|
28
|
+
on_overflow: "continue"
|
|
29
|
+
buffer_tokens: 20000
|
|
30
|
+
|
|
31
|
+
agent:
|
|
32
|
+
max_turns: 500
|
|
33
|
+
default_context_window: 200000
|
|
34
|
+
|
|
35
|
+
ui:
|
|
36
|
+
theme: "gruvbox-dark"
|
|
37
|
+
collapse_thinking: true
|
|
38
|
+
show_welcome_shortcuts: true
|
|
39
|
+
hidden_models: []
|
|
40
|
+
|
|
41
|
+
permissions:
|
|
42
|
+
mode: "prompt"
|
|
43
|
+
|
|
44
|
+
notifications:
|
|
45
|
+
enabled: false
|
|
46
|
+
volume: 0.5
|
|
47
|
+
|
|
48
|
+
# Example: DeepSeek provider
|
|
49
|
+
# export DEEPSEEK_API_KEY="sk-your-api-key"
|
|
50
|
+
#
|
|
51
|
+
# Then set in [llm]:
|
|
52
|
+
# default_provider = "deepseek"
|
|
53
|
+
# default_model = "deepseek-v4-flash"
|
vtx/diff_display.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
DIFF_BG_PAD_MARKER = "__VTX_DIFF_BG_PAD__"
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def blend_hex(fg_hex: str, bg_hex: str, alpha: float = 0.15) -> str:
|
|
5
|
+
fg_hex = fg_hex.lstrip("#")
|
|
6
|
+
bg_hex = bg_hex.lstrip("#")
|
|
7
|
+
fr, fg, fb = int(fg_hex[:2], 16), int(fg_hex[2:4], 16), int(fg_hex[4:6], 16)
|
|
8
|
+
br, bg_c, bb = int(bg_hex[:2], 16), int(bg_hex[2:4], 16), int(bg_hex[4:6], 16)
|
|
9
|
+
r = int(fr * alpha + br * (1 - alpha))
|
|
10
|
+
g = int(fg * alpha + bg_c * (1 - alpha))
|
|
11
|
+
b = int(fb * alpha + bb * (1 - alpha))
|
|
12
|
+
return f"#{r:02x}{g:02x}{b:02x}"
|