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.
Files changed (90) hide show
  1. {vamp_cli-0.1.6/src/vamp_cli.egg-info → vamp_cli-0.1.8}/PKG-INFO +1 -1
  2. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/pyproject.toml +2 -2
  3. vamp_cli-0.1.8/src/vamp_cli/boot/bootstrap.py +95 -0
  4. vamp_cli-0.1.8/src/vamp_cli/boot/main.py +418 -0
  5. vamp_cli-0.1.8/src/vamp_cli/boot/streaming.py +126 -0
  6. vamp_cli-0.1.8/src/vamp_cli/cli.py +64 -0
  7. vamp_cli-0.1.8/src/vamp_cli/commands/branches.py +67 -0
  8. vamp_cli-0.1.8/src/vamp_cli/commands/compact.py +60 -0
  9. vamp_cli-0.1.8/src/vamp_cli/commands/config.py +27 -0
  10. vamp_cli-0.1.8/src/vamp_cli/commands/exit.py +16 -0
  11. vamp_cli-0.1.8/src/vamp_cli/commands/fork.py +154 -0
  12. vamp_cli-0.1.8/src/vamp_cli/commands/mode.py +16 -0
  13. vamp_cli-0.1.8/src/vamp_cli/config/editor.py +30 -0
  14. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/persistence/event_log.py +1 -0
  15. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/persistence/loader.py +3 -0
  16. vamp_cli-0.1.8/src/vamp_cli/ui/banners.py +41 -0
  17. vamp_cli-0.1.8/src/vamp_cli/ui/render.py +42 -0
  18. vamp_cli-0.1.8/src/vamp_cli/utils/__init__.py +0 -0
  19. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/utils/auto_complete.py +5 -6
  20. vamp_cli-0.1.8/src/vamp_cli/workspace/__init__.py +0 -0
  21. {vamp_cli-0.1.6 → vamp_cli-0.1.8/src/vamp_cli.egg-info}/PKG-INFO +1 -1
  22. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli.egg-info/SOURCES.txt +15 -1
  23. vamp_cli-0.1.8/src/vamp_cli.egg-info/entry_points.txt +2 -0
  24. vamp_cli-0.1.6/src/vamp_cli/main.py +0 -901
  25. vamp_cli-0.1.6/src/vamp_cli.egg-info/entry_points.txt +0 -2
  26. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/LICENSE +0 -0
  27. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/MANIFEST.in +0 -0
  28. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/README.md +0 -0
  29. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/setup.cfg +0 -0
  30. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/__init__.py +0 -0
  31. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/agent/__init__.py +0 -0
  32. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/agent/context_sync.py +0 -0
  33. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/agent/graph.py +0 -0
  34. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/agent/instructions.py +0 -0
  35. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/agent/nodes.py +0 -0
  36. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/agent/roles.py +0 -0
  37. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/agent/runtime/__init__.py +0 -0
  38. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/agent/runtime/runtime.py +0 -0
  39. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/agent/state.py +0 -0
  40. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/agent/status.py +0 -0
  41. {vamp_cli-0.1.6/src/vamp_cli/config → vamp_cli-0.1.8/src/vamp_cli/commands}/__init__.py +0 -0
  42. {vamp_cli-0.1.6/src/vamp_cli/llm → vamp_cli-0.1.8/src/vamp_cli/config}/__init__.py +0 -0
  43. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/config/default_config.yaml +0 -0
  44. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/config/loader.py +0 -0
  45. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/config/schema.py +0 -0
  46. {vamp_cli-0.1.6/src/vamp_cli/sandbox → vamp_cli-0.1.8/src/vamp_cli/llm}/__init__.py +0 -0
  47. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/llm/adapter.py +0 -0
  48. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/llm/adapter_registry.py +0 -0
  49. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/llm/debug_callbacks.py +0 -0
  50. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/llm/router.py +0 -0
  51. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/llm/usage.py +0 -0
  52. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/logging_setup/logging_setup.py +0 -0
  53. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/persistence/__init__.py +0 -0
  54. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/persistence/compaction.py +0 -0
  55. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/persistence/emitter.py +0 -0
  56. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/persistence/fork.py +0 -0
  57. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/persistence/transcript.py +0 -0
  58. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/prompts/agent.md +0 -0
  59. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/prompts/coder.md +0 -0
  60. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/prompts/planner.md +0 -0
  61. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/prompts/prompts.py +0 -0
  62. {vamp_cli-0.1.6/src/vamp_cli/tools → vamp_cli-0.1.8/src/vamp_cli/sandbox}/__init__.py +0 -0
  63. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/sandbox/base.py +0 -0
  64. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/sandbox/config.py +0 -0
  65. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/sandbox/docker.py +0 -0
  66. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/sandbox/manager.py +0 -0
  67. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/sandbox/result.py +0 -0
  68. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/sandbox/terminal.py +0 -0
  69. {vamp_cli-0.1.6/src/vamp_cli/utils → vamp_cli-0.1.8/src/vamp_cli/tools}/__init__.py +0 -0
  70. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/tools/base.py +0 -0
  71. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/tools/diagnostic.py +0 -0
  72. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/tools/execute_cmd.py +0 -0
  73. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/tools/files.py +0 -0
  74. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/tools/registry.py +0 -0
  75. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/tools/tools_classes.py +0 -0
  76. {vamp_cli-0.1.6/src/vamp_cli/workspace → vamp_cli-0.1.8/src/vamp_cli/ui}/__init__.py +0 -0
  77. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/utils/error_codes.py +0 -0
  78. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/utils/ignore.py +0 -0
  79. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/utils/token_tracker.py +0 -0
  80. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli/workspace/workspace.py +0 -0
  81. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli.egg-info/dependency_links.txt +0 -0
  82. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli.egg-info/requires.txt +0 -0
  83. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/src/vamp_cli.egg-info/top_level.txt +0 -0
  84. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/tests/test_list_dir.py +0 -0
  85. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/tests/test_loader.py +0 -0
  86. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/tests/test_model_fallbacks.py +0 -0
  87. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/tests/test_persistence.py +0 -0
  88. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/tests/test_registry_async.py +0 -0
  89. {vamp_cli-0.1.6 → vamp_cli-0.1.8}/tests/test_sandbox_roundtrip.py +0 -0
  90. {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.6
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.6"
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.main:start"
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