vamp-cli 0.1.6__tar.gz → 0.1.8__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.
- {vamp_cli-0.1.6/src/vamp_cli.egg-info → vamp_cli-0.1.8}/PKG-INFO +1 -1
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/pyproject.toml +2 -2
- vamp_cli-0.1.8/src/vamp_cli/boot/bootstrap.py +95 -0
- vamp_cli-0.1.8/src/vamp_cli/boot/main.py +418 -0
- vamp_cli-0.1.8/src/vamp_cli/boot/streaming.py +126 -0
- vamp_cli-0.1.8/src/vamp_cli/cli.py +64 -0
- vamp_cli-0.1.8/src/vamp_cli/commands/branches.py +67 -0
- vamp_cli-0.1.8/src/vamp_cli/commands/compact.py +60 -0
- vamp_cli-0.1.8/src/vamp_cli/commands/config.py +27 -0
- vamp_cli-0.1.8/src/vamp_cli/commands/exit.py +16 -0
- vamp_cli-0.1.8/src/vamp_cli/commands/fork.py +154 -0
- vamp_cli-0.1.8/src/vamp_cli/commands/mode.py +16 -0
- vamp_cli-0.1.8/src/vamp_cli/config/editor.py +30 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/persistence/event_log.py +1 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/persistence/loader.py +3 -0
- vamp_cli-0.1.8/src/vamp_cli/ui/banners.py +41 -0
- vamp_cli-0.1.8/src/vamp_cli/ui/render.py +42 -0
- vamp_cli-0.1.8/src/vamp_cli/utils/__init__.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/utils/auto_complete.py +5 -6
- vamp_cli-0.1.8/src/vamp_cli/workspace/__init__.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8/src/vamp_cli.egg-info}/PKG-INFO +1 -1
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli.egg-info/SOURCES.txt +15 -1
- vamp_cli-0.1.8/src/vamp_cli.egg-info/entry_points.txt +2 -0
- vamp_cli-0.1.6/src/vamp_cli/main.py +0 -901
- vamp_cli-0.1.6/src/vamp_cli.egg-info/entry_points.txt +0 -2
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/LICENSE +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/MANIFEST.in +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/README.md +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/setup.cfg +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/__init__.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/agent/__init__.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/agent/context_sync.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/agent/graph.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/agent/instructions.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/agent/nodes.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/agent/roles.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/agent/runtime/__init__.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/agent/runtime/runtime.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/agent/state.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/agent/status.py +0 -0
- {vamp_cli-0.1.6/src/vamp_cli/config → vamp_cli-0.1.8/src/vamp_cli/commands}/__init__.py +0 -0
- {vamp_cli-0.1.6/src/vamp_cli/llm → vamp_cli-0.1.8/src/vamp_cli/config}/__init__.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/config/default_config.yaml +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/config/loader.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/config/schema.py +0 -0
- {vamp_cli-0.1.6/src/vamp_cli/sandbox → vamp_cli-0.1.8/src/vamp_cli/llm}/__init__.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/llm/adapter.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/llm/adapter_registry.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/llm/debug_callbacks.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/llm/router.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/llm/usage.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/logging_setup/logging_setup.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/persistence/__init__.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/persistence/compaction.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/persistence/emitter.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/persistence/fork.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/persistence/transcript.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/prompts/agent.md +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/prompts/coder.md +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/prompts/planner.md +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/prompts/prompts.py +0 -0
- {vamp_cli-0.1.6/src/vamp_cli/tools → vamp_cli-0.1.8/src/vamp_cli/sandbox}/__init__.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/sandbox/base.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/sandbox/config.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/sandbox/docker.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/sandbox/manager.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/sandbox/result.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/sandbox/terminal.py +0 -0
- {vamp_cli-0.1.6/src/vamp_cli/utils → vamp_cli-0.1.8/src/vamp_cli/tools}/__init__.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/tools/base.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/tools/diagnostic.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/tools/execute_cmd.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/tools/files.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/tools/registry.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/tools/tools_classes.py +0 -0
- {vamp_cli-0.1.6/src/vamp_cli/workspace → vamp_cli-0.1.8/src/vamp_cli/ui}/__init__.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/utils/error_codes.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/utils/ignore.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/utils/token_tracker.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/workspace/workspace.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli.egg-info/dependency_links.txt +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli.egg-info/requires.txt +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli.egg-info/top_level.txt +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/tests/test_list_dir.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/tests/test_loader.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/tests/test_model_fallbacks.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/tests/test_persistence.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/tests/test_registry_async.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/tests/test_sandbox_roundtrip.py +0 -0
- {vamp_cli-0.1.6 → vamp_cli-0.1.8}/tests/test_system_verification.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vamp-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.8
|
|
4
4
|
Summary: VAMP-CLI is an interactive coding agent for working in a local repository. It combines a planner, a coder, model fallbacks, workspace-aware file tools, and a Docker-backed command sandbox in one terminal application. The agent can inspect and edit files, search a codebase, run commands, maintain a task list, and ask for confirmation before executing potentially dangerous operations.
|
|
5
5
|
Author-email: Satyam Kumar <satkr2208@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vamp-cli"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.8"
|
|
8
8
|
authors = [
|
|
9
9
|
{ name = "Satyam Kumar", email = "satkr2208@gmail.com" }
|
|
10
10
|
]
|
|
@@ -48,7 +48,7 @@ vamp_cli = ["*.md", "**/*.md", "**/*.yaml"]
|
|
|
48
48
|
|
|
49
49
|
|
|
50
50
|
[project.scripts]
|
|
51
|
-
vamp = "vamp_cli.
|
|
51
|
+
vamp = "vamp_cli.cli:start"
|
|
52
52
|
|
|
53
53
|
[tool.pytest.ini_options]
|
|
54
54
|
testpaths = ["tests"]
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from vamp_cli.workspace.workspace import Workspace
|
|
5
|
+
from vamp_cli.config.loader import load_config
|
|
6
|
+
from vamp_cli.sandbox.terminal import Terminal
|
|
7
|
+
from vamp_cli.sandbox.docker import SandboxConfig
|
|
8
|
+
from vamp_cli.sandbox.manager import SandboxManager
|
|
9
|
+
from vamp_cli.tools.files import FileTools
|
|
10
|
+
from vamp_cli.tools.diagnostic import DiagnosticTools
|
|
11
|
+
from vamp_cli.tools.execute_cmd import ExecuteCommandTool
|
|
12
|
+
from vamp_cli.tools.tools_classes import *
|
|
13
|
+
from vamp_cli.tools.registry import ToolRegistry
|
|
14
|
+
from vamp_cli.agent.runtime.runtime import AgentRuntime
|
|
15
|
+
from vamp_cli.utils.ignore import IgnoreMatcher
|
|
16
|
+
from vamp_cli.prompts.prompts import PromptStore
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def build_startup(project_dir: str):
|
|
21
|
+
|
|
22
|
+
# services
|
|
23
|
+
workspace = Workspace(root=project_dir)
|
|
24
|
+
ignore = IgnoreMatcher(repo_root=str(workspace.root))
|
|
25
|
+
diagnostic = DiagnosticTools(workspace=workspace)
|
|
26
|
+
sandbox_config = SandboxConfig()
|
|
27
|
+
# d_sandbox = DockerSandbox(workspace, sandbox_config)
|
|
28
|
+
d_sandbox = SandboxManager(sandbox_config)
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
# d_sandbox.start()
|
|
32
|
+
sandbox = await d_sandbox.create(workspace)
|
|
33
|
+
except Exception as e:
|
|
34
|
+
print(f"\n\nDocker not running. Please start docker.\n\n")
|
|
35
|
+
sys.exit(0)
|
|
36
|
+
|
|
37
|
+
terminal = Terminal(sandbox=sandbox)
|
|
38
|
+
|
|
39
|
+
file_tools = FileTools(
|
|
40
|
+
workspace=workspace,
|
|
41
|
+
ignore=ignore,
|
|
42
|
+
diagnostic=diagnostic,
|
|
43
|
+
)
|
|
44
|
+
exec_tool = ExecuteCommandTool(terminal=terminal)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# tool registry and tool wrappers
|
|
48
|
+
registry = ToolRegistry()
|
|
49
|
+
|
|
50
|
+
tools = [
|
|
51
|
+
ListDirTool(file_tools, registry),
|
|
52
|
+
ReadFileTool(file_tools, registry),
|
|
53
|
+
ReadManyFilesTool(file_tools, registry),
|
|
54
|
+
WriteFileTool(file_tools, registry),
|
|
55
|
+
ApplySearchReplaceTool(file_tools, registry),
|
|
56
|
+
FileSearchTool(file_tools, registry),
|
|
57
|
+
RipgrepSearchTool(file_tools, registry),
|
|
58
|
+
ExecuteCommandToolDef(exec_tool, registry),
|
|
59
|
+
SubmitPlanTool(registry),
|
|
60
|
+
WriteTodosTool(registry),
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
registry.register_many(tools)
|
|
64
|
+
|
|
65
|
+
return registry, d_sandbox
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
async def create_runtime(project_dir: str):
|
|
69
|
+
|
|
70
|
+
from vamp_cli.llm.router import ModelRouter
|
|
71
|
+
from vamp_cli.llm.adapter_registry import AdapterRegistry
|
|
72
|
+
|
|
73
|
+
config = load_config()
|
|
74
|
+
|
|
75
|
+
adapter = AdapterRegistry()
|
|
76
|
+
|
|
77
|
+
tool_registry, sandbox = await build_startup(project_dir=project_dir)
|
|
78
|
+
|
|
79
|
+
prompt_store = PromptStore(hot_reload=True)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
model_router = ModelRouter(config=config, adapters=adapter)
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
AgentRuntime(
|
|
86
|
+
config=config,
|
|
87
|
+
model_router=model_router,
|
|
88
|
+
tool_registry=tool_registry,
|
|
89
|
+
prompt_store=prompt_store,
|
|
90
|
+
),
|
|
91
|
+
tool_registry,
|
|
92
|
+
sandbox,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
from rich.console import Console
|
|
2
|
+
from rich.panel import Panel
|
|
3
|
+
from rich.markdown import Markdown
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from prompt_toolkit import PromptSession
|
|
8
|
+
from prompt_toolkit.patch_stdout import patch_stdout
|
|
9
|
+
from prompt_toolkit.formatted_text import HTML
|
|
10
|
+
|
|
11
|
+
from vamp_cli.utils.token_tracker import Token_Tracker
|
|
12
|
+
from vamp_cli.utils.auto_complete import AtFileCompleter, expand_at_references
|
|
13
|
+
from vamp_cli.ui.banners import print_banner
|
|
14
|
+
from vamp_cli.ui.render import render_context_bar, render_tool_interrupt
|
|
15
|
+
from vamp_cli.boot.bootstrap import create_runtime
|
|
16
|
+
from vamp_cli.boot.streaming import streaming
|
|
17
|
+
|
|
18
|
+
from vamp_cli.commands.exit import exit_cli
|
|
19
|
+
from vamp_cli.commands.mode import mode
|
|
20
|
+
from vamp_cli.commands.config import config
|
|
21
|
+
from vamp_cli.commands.branches import branches
|
|
22
|
+
from vamp_cli.commands.fork import fork
|
|
23
|
+
from vamp_cli.commands.compact import compact
|
|
24
|
+
|
|
25
|
+
from vamp_cli.agent.graph import create_graph
|
|
26
|
+
from vamp_cli.agent.roles import AgentRole
|
|
27
|
+
from vamp_cli.agent.status import FeedbackResponse
|
|
28
|
+
from vamp_cli.agent.context_sync import mode_switch_sync
|
|
29
|
+
from vamp_cli.agent.instructions import TODO_INSTRUCTIONS
|
|
30
|
+
from vamp_cli.persistence.emitter import EventEmitter
|
|
31
|
+
from vamp_cli.persistence.event_log import EventLog, uuid7
|
|
32
|
+
from vamp_cli.persistence.transcript import print_transcript
|
|
33
|
+
from vamp_cli.persistence.compaction import DEFAULT_KEEP_TURNS, auto_compact_if_needed, DEFAULT_COMPACT_THRESHOLD
|
|
34
|
+
from vamp_cli.persistence.loader import (
|
|
35
|
+
load_session,
|
|
36
|
+
ResumedSession,
|
|
37
|
+
load_chain,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
from langgraph.types import Command
|
|
41
|
+
from langchain_core.runnables import RunnableConfig
|
|
42
|
+
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
|
|
43
|
+
from langchain_core.messages import HumanMessage, SystemMessage
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async def main(
|
|
48
|
+
version: str,
|
|
49
|
+
console: Console,
|
|
50
|
+
project_dir: str,
|
|
51
|
+
resume_id: str | None = None
|
|
52
|
+
):
|
|
53
|
+
|
|
54
|
+
VERSION = version
|
|
55
|
+
|
|
56
|
+
token_tracker = Token_Tracker()
|
|
57
|
+
agent_runtime, tool_registry, sandbox = await create_runtime(project_dir)
|
|
58
|
+
coder_profile = agent_runtime.resolve(AgentRole.CODER)
|
|
59
|
+
planner_profile = agent_runtime.resolve(AgentRole.PLANNER)
|
|
60
|
+
|
|
61
|
+
is_resume = resume_id is not None
|
|
62
|
+
|
|
63
|
+
SESSION_ID = resume_id if is_resume else str(uuid7())
|
|
64
|
+
|
|
65
|
+
sessions_dir = Path.home() / ".vamp" / "sessions"
|
|
66
|
+
checkpoint_dir = Path.home() / ".vamp" / "checkpoints"
|
|
67
|
+
checkpoint_dir.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
checkpoint_db = checkpoint_dir / f"{SESSION_ID}.sqlite"
|
|
69
|
+
|
|
70
|
+
async with AsyncSqliteSaver.from_conn_string(str(checkpoint_db)) as checkpointer:
|
|
71
|
+
|
|
72
|
+
graph = create_graph(
|
|
73
|
+
coder_model=coder_profile.model,
|
|
74
|
+
planner_model=planner_profile.model,
|
|
75
|
+
tool_registry=tool_registry,
|
|
76
|
+
checkpointer=checkpointer,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
log = await EventLog.open(
|
|
80
|
+
session_id=SESSION_ID,
|
|
81
|
+
workspace_root=project_dir,
|
|
82
|
+
version=VERSION,
|
|
83
|
+
)
|
|
84
|
+
emitter = EventEmitter(log)
|
|
85
|
+
|
|
86
|
+
last_session = sessions_dir / "last"
|
|
87
|
+
last_session.mkdir(parents=True, exist_ok=True)
|
|
88
|
+
(last_session / "last.head").write_text(SESSION_ID, encoding="utf-8")
|
|
89
|
+
|
|
90
|
+
THREAD_ID = SESSION_ID
|
|
91
|
+
session_config: RunnableConfig = {
|
|
92
|
+
"configurable": {"thread_id": THREAD_ID, "emitter": emitter},
|
|
93
|
+
"recursion_limit": 200,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# fresh session
|
|
97
|
+
if not is_resume:
|
|
98
|
+
await log.append("session_start", {
|
|
99
|
+
"workspace_root": project_dir,
|
|
100
|
+
"models": {
|
|
101
|
+
"coder": getattr(coder_profile.model, "model_name", None)
|
|
102
|
+
or getattr(coder_profile.model, "model", None),
|
|
103
|
+
"planner": getattr(planner_profile.model, "model_name", None)
|
|
104
|
+
or getattr(planner_profile.model, "model", None),
|
|
105
|
+
},
|
|
106
|
+
"roles": {"coder": AgentRole.CODER.value, "planner": AgentRole.PLANNER.value},
|
|
107
|
+
"tools": tool_registry.list_tool_names(),
|
|
108
|
+
"checkpoint_thread_id": THREAD_ID, # step 4 will make this per-session
|
|
109
|
+
"checkpoint_backend": "sqlite",
|
|
110
|
+
"system_prompts": {
|
|
111
|
+
"coder": coder_profile.system_prompt,
|
|
112
|
+
"planner": planner_profile.system_prompt,
|
|
113
|
+
},
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
selected_mode: str = "normal"
|
|
118
|
+
executed_mode: str = "normal"
|
|
119
|
+
is_session_started = False
|
|
120
|
+
pending_command = None
|
|
121
|
+
last_state = None
|
|
122
|
+
|
|
123
|
+
coder_name = getattr(coder_profile.model, "model_name", None) or getattr(coder_profile.model, "model", "default")
|
|
124
|
+
planner_name = getattr(planner_profile.model, "model_name", None) or getattr(planner_profile.model, "model", "default")
|
|
125
|
+
|
|
126
|
+
print_banner(console, SESSION_ID, is_resume, str(coder_name), str(planner_name), version, project_dir)
|
|
127
|
+
|
|
128
|
+
resumed: ResumedSession | None = None
|
|
129
|
+
if is_resume:
|
|
130
|
+
try:
|
|
131
|
+
resumed = await load_session(
|
|
132
|
+
sessions_dir=sessions_dir,
|
|
133
|
+
session_id=SESSION_ID,
|
|
134
|
+
graph=graph,
|
|
135
|
+
session_config=session_config,
|
|
136
|
+
reconstruct_if_missing=True,
|
|
137
|
+
)
|
|
138
|
+
selected_mode = resumed.selected_mode
|
|
139
|
+
executed_mode = resumed.executed_mode
|
|
140
|
+
is_session_started = resumed.is_session_started
|
|
141
|
+
last_state = resumed.last_state
|
|
142
|
+
if resumed.thread_id and resumed.thread_id != THREAD_ID:
|
|
143
|
+
THREAD_ID = resumed.thread_id
|
|
144
|
+
session_config["configurable"]["thread_id"] = THREAD_ID
|
|
145
|
+
|
|
146
|
+
print_transcript(resumed.events, header=f"Resumed Session {SESSION_ID[:8]}")
|
|
147
|
+
except FileNotFoundError as e:
|
|
148
|
+
await sandbox.destroy()
|
|
149
|
+
console.print(f"[bold red][resume error][/bold red] {e}")
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# Bottom toolbar dynamic status generator
|
|
155
|
+
def get_bottom_toolbar():
|
|
156
|
+
ctx_limit = planner_profile.context_window if selected_mode == "plan" else coder_profile.context_window
|
|
157
|
+
used = emitter.last_input_tokens
|
|
158
|
+
pct = f"{(used / ctx_limit * 100):.1f}%" if ctx_limit and used else "0%"
|
|
159
|
+
return HTML(
|
|
160
|
+
f" <b>Mode:</b> <style bg='ansimagenta'>{selected_mode.upper()}</style> | "
|
|
161
|
+
f"<b>Thread:</b> {THREAD_ID} | "
|
|
162
|
+
f"<b>Context:</b> {used:,}/{ctx_limit:,} ({pct}) | "
|
|
163
|
+
f"<b>@file</b> for files"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
session = PromptSession(
|
|
167
|
+
completer=AtFileCompleter(root_dir=project_dir),
|
|
168
|
+
bottom_toolbar=get_bottom_toolbar,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
if is_resume and resumed and resumed.pending_interrupt:
|
|
173
|
+
pi = resumed.pending_interrupt
|
|
174
|
+
checkpoint_data = pi.get("payload") or {}
|
|
175
|
+
|
|
176
|
+
if checkpoint_data.get("implementation_permission"):
|
|
177
|
+
interrupt_kind = "plan_approval"
|
|
178
|
+
console.print(
|
|
179
|
+
Panel(
|
|
180
|
+
Markdown(checkpoint_data.get("plan", "")),
|
|
181
|
+
title="[bold yellow] Plan Implementation Review[/bold yellow]",
|
|
182
|
+
border_style="yellow",
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
elif checkpoint_data.get("requires_tool_approval"):
|
|
186
|
+
interrupt_kind = "tool_approval"
|
|
187
|
+
render_tool_interrupt(console, checkpoint_data)
|
|
188
|
+
else:
|
|
189
|
+
interrupt_kind = "unknown"
|
|
190
|
+
console.print(Panel(str(checkpoint_data), title="Interrupt Review"))
|
|
191
|
+
|
|
192
|
+
with patch_stdout():
|
|
193
|
+
choice = (await session.prompt_async(HTML("<b>Authorize action? (y/n): </b>"))).strip().lower()
|
|
194
|
+
|
|
195
|
+
if choice in ("y", "yes"):
|
|
196
|
+
console.print("[green]Authorized. Resuming execution...[/green]")
|
|
197
|
+
await log.append("interrupt_response", {"kind": interrupt_kind, "approved": True, "reason": None})
|
|
198
|
+
pending_command = Command(resume=FeedbackResponse(approved=True))
|
|
199
|
+
else:
|
|
200
|
+
with patch_stdout():
|
|
201
|
+
reason = (await session.prompt_async(HTML("<b>Rejection reason: </b>"))).strip()
|
|
202
|
+
await log.append("interrupt_response", {"kind": interrupt_kind, "approved": False, "reason": reason})
|
|
203
|
+
console.print("[red]Blocked. Reporting reason back to agent...[/red]")
|
|
204
|
+
pending_command = Command(resume=FeedbackResponse(approved=False, reason=reason))
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
while True:
|
|
209
|
+
try:
|
|
210
|
+
if pending_command is None:
|
|
211
|
+
with patch_stdout():
|
|
212
|
+
user_input = (
|
|
213
|
+
await session.prompt_async(HTML("<b><ansicyan>❯</ansicyan></b> "))
|
|
214
|
+
).strip()
|
|
215
|
+
|
|
216
|
+
if not user_input:
|
|
217
|
+
continue
|
|
218
|
+
|
|
219
|
+
user_input = expand_at_references(user_input, project_dir)
|
|
220
|
+
|
|
221
|
+
# /exit or /quit
|
|
222
|
+
if user_input.lower() in ["/exit", "/quit"]:
|
|
223
|
+
await exit_cli(console, log, sandbox)
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
# /mode
|
|
227
|
+
if user_input == "/mode":
|
|
228
|
+
output = await mode(console, log, executed_mode, session)
|
|
229
|
+
if isinstance(output, str):
|
|
230
|
+
selected_mode = output
|
|
231
|
+
continue
|
|
232
|
+
|
|
233
|
+
# /config
|
|
234
|
+
if user_input == "/config":
|
|
235
|
+
# open ~/.vamp/config.yaml
|
|
236
|
+
config()
|
|
237
|
+
continue
|
|
238
|
+
|
|
239
|
+
# /branches
|
|
240
|
+
if user_input == "/branches":
|
|
241
|
+
continue_flag = await branches(
|
|
242
|
+
console,
|
|
243
|
+
log,
|
|
244
|
+
session,
|
|
245
|
+
sessions_dir,
|
|
246
|
+
SESSION_ID,
|
|
247
|
+
session_config
|
|
248
|
+
)
|
|
249
|
+
if continue_flag:
|
|
250
|
+
continue
|
|
251
|
+
|
|
252
|
+
# /fork
|
|
253
|
+
if user_input == "/fork" or user_input.startswith("/fork "):
|
|
254
|
+
|
|
255
|
+
output = await fork(
|
|
256
|
+
console,
|
|
257
|
+
log,
|
|
258
|
+
session,
|
|
259
|
+
sessions_dir,
|
|
260
|
+
SESSION_ID,
|
|
261
|
+
session_config,
|
|
262
|
+
user_input,
|
|
263
|
+
graph
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
if isinstance(output, bool):
|
|
267
|
+
if output:
|
|
268
|
+
continue
|
|
269
|
+
elif isinstance(output, tuple):
|
|
270
|
+
fg, selected_mode, executed_mode, is_session_started = output
|
|
271
|
+
if fg:
|
|
272
|
+
continue
|
|
273
|
+
|
|
274
|
+
# /compact
|
|
275
|
+
if user_input.startswith("/compact"):
|
|
276
|
+
cont_flag = await compact(
|
|
277
|
+
console,
|
|
278
|
+
log,
|
|
279
|
+
session,
|
|
280
|
+
sessions_dir,
|
|
281
|
+
SESSION_ID,
|
|
282
|
+
session_config,
|
|
283
|
+
selected_mode,
|
|
284
|
+
coder_profile,
|
|
285
|
+
planner_profile,
|
|
286
|
+
graph
|
|
287
|
+
)
|
|
288
|
+
if cont_flag:
|
|
289
|
+
continue
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
token_tracker.start()
|
|
293
|
+
new_mode = selected_mode
|
|
294
|
+
await log.append("user_message", {"content": user_input, "mode": new_mode})
|
|
295
|
+
|
|
296
|
+
# Auto-compaction hook
|
|
297
|
+
chain = load_chain(sessions_dir, SESSION_ID)
|
|
298
|
+
auto = await auto_compact_if_needed(
|
|
299
|
+
log,
|
|
300
|
+
chain=chain,
|
|
301
|
+
graph=graph,
|
|
302
|
+
session_config=session_config,
|
|
303
|
+
model=coder_profile.model if new_mode == "normal" else planner_profile.model,
|
|
304
|
+
last_input_tokens=emitter.last_input_tokens,
|
|
305
|
+
context_window=coder_profile.context_window if new_mode == "normal" else planner_profile.context_window,
|
|
306
|
+
threshold=DEFAULT_COMPACT_THRESHOLD,
|
|
307
|
+
keep_turns=DEFAULT_KEEP_TURNS,
|
|
308
|
+
)
|
|
309
|
+
if auto:
|
|
310
|
+
last_state = auto["new_state"]
|
|
311
|
+
emitter.last_input_tokens = 0
|
|
312
|
+
console.print("[dim yellow] Auto-compacted earlier turns to preserve token headroom.[/dim yellow]")
|
|
313
|
+
|
|
314
|
+
if not is_session_started:
|
|
315
|
+
payload = {
|
|
316
|
+
"planner_messages": [
|
|
317
|
+
SystemMessage(content=planner_profile.system_prompt, id="planner_system_prompt"),
|
|
318
|
+
HumanMessage(content=user_input),
|
|
319
|
+
],
|
|
320
|
+
"coder_messages": [
|
|
321
|
+
SystemMessage(content=coder_profile.system_prompt, id="coder_system_prompt"),
|
|
322
|
+
SystemMessage(content=TODO_INSTRUCTIONS),
|
|
323
|
+
HumanMessage(content=user_input),
|
|
324
|
+
],
|
|
325
|
+
"task": user_input,
|
|
326
|
+
"mode": new_mode,
|
|
327
|
+
"plan": None,
|
|
328
|
+
"requires_execution": False,
|
|
329
|
+
"todo": [],
|
|
330
|
+
"iteration": 0,
|
|
331
|
+
"_pending_tool_channel": None,
|
|
332
|
+
"_pending_tool_calls": [],
|
|
333
|
+
"_dangerous_tool_call_ids": [],
|
|
334
|
+
"_approved_tool_call_ids": [],
|
|
335
|
+
"_denied_tool_call_ids": [],
|
|
336
|
+
}
|
|
337
|
+
is_session_started = True
|
|
338
|
+
else:
|
|
339
|
+
sync = mode_switch_sync(prev_mode=executed_mode, new_mode=new_mode, last_state=last_state)
|
|
340
|
+
msg_type = "planner_messages" if new_mode == "plan" else "coder_messages"
|
|
341
|
+
payload = {
|
|
342
|
+
msg_type: sync.get(msg_type, []) + [HumanMessage(content=user_input)],
|
|
343
|
+
"mode": new_mode,
|
|
344
|
+
"iteration": 0,
|
|
345
|
+
}
|
|
346
|
+
if new_mode == "plan":
|
|
347
|
+
payload["is_plan_injected"] = False
|
|
348
|
+
if new_mode == "normal" and executed_mode == "plan":
|
|
349
|
+
payload["plan"] = ""
|
|
350
|
+
|
|
351
|
+
executed_mode = new_mode
|
|
352
|
+
|
|
353
|
+
else:
|
|
354
|
+
payload = pending_command
|
|
355
|
+
pending_command = None
|
|
356
|
+
|
|
357
|
+
# Run graph stream
|
|
358
|
+
usage = await streaming(console, graph, payload, session_config, emitter, tool_registry)
|
|
359
|
+
token_tracker.add(usage)
|
|
360
|
+
|
|
361
|
+
# Interruption verification
|
|
362
|
+
state_snapshot = await graph.aget_state(session_config)
|
|
363
|
+
last_state = state_snapshot.values
|
|
364
|
+
active_interrupts = getattr(state_snapshot, "interrupts", [])
|
|
365
|
+
if not active_interrupts and state_snapshot.tasks:
|
|
366
|
+
for task in state_snapshot.tasks:
|
|
367
|
+
if getattr(task, "interrupts", None):
|
|
368
|
+
active_interrupts = task.interrupts
|
|
369
|
+
break
|
|
370
|
+
|
|
371
|
+
if active_interrupts:
|
|
372
|
+
raw_interrupt = active_interrupts[0]
|
|
373
|
+
checkpoint_data = getattr(raw_interrupt, "value", raw_interrupt)
|
|
374
|
+
|
|
375
|
+
if checkpoint_data.get("implementation_permission"): # type: ignore
|
|
376
|
+
interrupt_kind = "plan_approval"
|
|
377
|
+
console.print(
|
|
378
|
+
Panel(
|
|
379
|
+
Markdown(checkpoint_data.get("plan", "")), # type: ignore
|
|
380
|
+
title="[bold yellow] Plan Implementation Review[/bold yellow]",
|
|
381
|
+
border_style="yellow",
|
|
382
|
+
)
|
|
383
|
+
)
|
|
384
|
+
elif checkpoint_data.get("requires_tool_approval"): # type: ignore
|
|
385
|
+
interrupt_kind = "tool_approval"
|
|
386
|
+
render_tool_interrupt(console, checkpoint_data) # type: ignore
|
|
387
|
+
else:
|
|
388
|
+
interrupt_kind = "unknown"
|
|
389
|
+
console.print(Panel(str(checkpoint_data), title="Interrupt Review"))
|
|
390
|
+
with patch_stdout():
|
|
391
|
+
choice = (await session.prompt_async(HTML("<b>Authorize action? (y/n): </b>"))).strip().lower()
|
|
392
|
+
|
|
393
|
+
if choice in ("y", "yes"):
|
|
394
|
+
console.print("[green]Authorized. Resuming execution...[/green]")
|
|
395
|
+
await log.append("interrupt_response", {"kind": interrupt_kind, "approved": True, "reason": None})
|
|
396
|
+
pending_command = Command(resume=FeedbackResponse(approved=True))
|
|
397
|
+
continue
|
|
398
|
+
else:
|
|
399
|
+
with patch_stdout():
|
|
400
|
+
reason = (await session.prompt_async(HTML("<b>Rejection reason: </b>"))).strip()
|
|
401
|
+
await log.append("interrupt_response", {"kind": interrupt_kind, "approved": False, "reason": reason})
|
|
402
|
+
console.print("[red]Blocked. Reporting reason back to agent...[/red]")
|
|
403
|
+
pending_command = Command(resume=FeedbackResponse(approved=False, reason=reason))
|
|
404
|
+
continue
|
|
405
|
+
|
|
406
|
+
tok_usage = token_tracker.close()
|
|
407
|
+
max_ctx = planner_profile.context_window if selected_mode == "plan" else coder_profile.context_window
|
|
408
|
+
render_context_bar(console, emitter.last_input_tokens, max_ctx, tok_usage)
|
|
409
|
+
|
|
410
|
+
except KeyboardInterrupt:
|
|
411
|
+
console.print("\n[dim]Action cancelled. Ready for input.[/dim]")
|
|
412
|
+
pending_command = None
|
|
413
|
+
continue
|
|
414
|
+
except Exception as e:
|
|
415
|
+
await log.close(reason="error")
|
|
416
|
+
await sandbox.destroy()
|
|
417
|
+
console.print(f"[bold red][runtime error][/bold red] {e}")
|
|
418
|
+
return
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from rich.console import Console, Group
|
|
2
|
+
from rich.live import Live
|
|
3
|
+
from rich.panel import Panel
|
|
4
|
+
from rich.text import Text
|
|
5
|
+
from rich.markdown import Markdown
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
|
|
9
|
+
from vamp_cli.tools.tools_classes import *
|
|
10
|
+
from vamp_cli.tools.registry import ToolRegistry
|
|
11
|
+
from vamp_cli.persistence.emitter import EventEmitter
|
|
12
|
+
|
|
13
|
+
from langgraph.types import Command
|
|
14
|
+
from langchain_core.runnables import RunnableConfig
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def streaming(
|
|
18
|
+
console: Console,
|
|
19
|
+
graph,
|
|
20
|
+
payload: dict | Command,
|
|
21
|
+
session_config: RunnableConfig,
|
|
22
|
+
emitter: EventEmitter,
|
|
23
|
+
tool_registry: ToolRegistry,
|
|
24
|
+
) -> dict[str, int]:
|
|
25
|
+
|
|
26
|
+
call_usage = {"input_tokens": 0, "output_tokens": 0, "total_tokens": 0}
|
|
27
|
+
|
|
28
|
+
# 1. state buffers for fixed zones
|
|
29
|
+
reasoning_buffer = ""
|
|
30
|
+
content_buffer = ""
|
|
31
|
+
|
|
32
|
+
# 2. Layout generator: Stacks Reasoning on top, Content on bottom
|
|
33
|
+
THINKING_MAX_LINES = 8
|
|
34
|
+
|
|
35
|
+
def generate_layout(is_final = False):
|
|
36
|
+
renderables = []
|
|
37
|
+
|
|
38
|
+
if reasoning_buffer:
|
|
39
|
+
lines = reasoning_buffer.strip().split("\n")
|
|
40
|
+
if not is_final and len(lines) > THINKING_MAX_LINES:
|
|
41
|
+
display_lines = ["... (scrolling)"] + lines[-(THINKING_MAX_LINES - 1):]
|
|
42
|
+
box_height = THINKING_MAX_LINES + 2
|
|
43
|
+
else:
|
|
44
|
+
display_lines = lines
|
|
45
|
+
box_height = None # Removes the height limit for the final render
|
|
46
|
+
|
|
47
|
+
display_text = "\n".join(display_lines)
|
|
48
|
+
renderables.append(
|
|
49
|
+
Panel(
|
|
50
|
+
Text(display_text, style="dim"),
|
|
51
|
+
title="[dim]Thinking[/dim]",
|
|
52
|
+
title_align="left",
|
|
53
|
+
border_style="dim"
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if content_buffer or is_final:
|
|
58
|
+
display_content = content_buffer if content_buffer else "**No response content or valid tool calls provided by agent.**"
|
|
59
|
+
renderables.append(
|
|
60
|
+
Panel(
|
|
61
|
+
Markdown(display_content),
|
|
62
|
+
title="[bold green]Agent[/bold green]",
|
|
63
|
+
title_align="left",
|
|
64
|
+
border_style="green"
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
if not renderables:
|
|
69
|
+
return Text("Starting...", style="dim italic")
|
|
70
|
+
|
|
71
|
+
return Group(*renderables)
|
|
72
|
+
|
|
73
|
+
# 3. Live Context Manager: Takes over rendering to update in-place
|
|
74
|
+
try:
|
|
75
|
+
with Live(generate_layout(), console=console, refresh_per_second=60, vertical_overflow="visible") as live:
|
|
76
|
+
|
|
77
|
+
# UI CALLBACK
|
|
78
|
+
def ui_tool_callback(tool_name: str, args: str, output_text: str):
|
|
79
|
+
nonlocal reasoning_buffer
|
|
80
|
+
# Append the tool's actual output directly to the reasoning panel
|
|
81
|
+
reasoning_buffer += f"\n>> {tool_name}: {args}\n\n"
|
|
82
|
+
live.update(generate_layout())
|
|
83
|
+
|
|
84
|
+
# INJECT IT TO TOOL REGISTRY
|
|
85
|
+
tool_registry.set_ui_callback(ui_tool_callback)
|
|
86
|
+
|
|
87
|
+
async for event in graph.astream_events(payload, config=session_config, version="v2"):
|
|
88
|
+
await emitter.consume(event)
|
|
89
|
+
kind = event["event"]
|
|
90
|
+
|
|
91
|
+
if kind == "on_chat_model_stream":
|
|
92
|
+
chunk = event["data"]["chunk"]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# Extract reasoning
|
|
96
|
+
reasoning_chunk = getattr(chunk, "reasoning_content", None)
|
|
97
|
+
if not reasoning_chunk and "reasoning_content" in chunk.additional_kwargs:
|
|
98
|
+
reasoning_chunk = chunk.additional_kwargs["reasoning_content"]
|
|
99
|
+
|
|
100
|
+
# Route text to the correct buffer
|
|
101
|
+
if reasoning_chunk:
|
|
102
|
+
reasoning_buffer += str(reasoning_chunk)
|
|
103
|
+
live.update(generate_layout())
|
|
104
|
+
|
|
105
|
+
if chunk.content:
|
|
106
|
+
content_buffer += str(chunk.content)
|
|
107
|
+
live.update(generate_layout())
|
|
108
|
+
|
|
109
|
+
elif kind == "on_chat_model_end":
|
|
110
|
+
output = event["data"].get("output")
|
|
111
|
+
um = getattr(output, "usage_metadata", None)
|
|
112
|
+
if um:
|
|
113
|
+
call_usage["input_tokens"] += um.get("input_tokens", 0)
|
|
114
|
+
call_usage["output_tokens"] += um.get("output_tokens", 0)
|
|
115
|
+
call_usage["total_tokens"] += um.get("total_tokens", 0)
|
|
116
|
+
|
|
117
|
+
live.update(generate_layout(is_final=True))
|
|
118
|
+
|
|
119
|
+
except (asyncio.CancelledError, KeyboardInterrupt):
|
|
120
|
+
console.print("\n[bold yellow]Action aborted by user.[/bold yellow]")
|
|
121
|
+
return call_usage
|
|
122
|
+
except Exception as e:
|
|
123
|
+
console.print(f"\n[bold red]Error during run:[/bold red] {e}")
|
|
124
|
+
return call_usage
|
|
125
|
+
|
|
126
|
+
return call_usage
|