vamp-cli 0.1.0__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.
Files changed (57) hide show
  1. vamp_cli/__init__.py +0 -0
  2. vamp_cli/agent/__init__.py +0 -0
  3. vamp_cli/agent/context_sync.py +72 -0
  4. vamp_cli/agent/graph.py +159 -0
  5. vamp_cli/agent/instructions.py +18 -0
  6. vamp_cli/agent/nodes.py +375 -0
  7. vamp_cli/agent/roles.py +8 -0
  8. vamp_cli/agent/runtime/__init__.py +0 -0
  9. vamp_cli/agent/runtime/runtime.py +133 -0
  10. vamp_cli/agent/state.py +55 -0
  11. vamp_cli/agent/status.py +17 -0
  12. vamp_cli/config/__init__.py +0 -0
  13. vamp_cli/config/loader.py +152 -0
  14. vamp_cli/config/schema.py +82 -0
  15. vamp_cli/llm/__init__.py +0 -0
  16. vamp_cli/llm/adapter.py +52 -0
  17. vamp_cli/llm/adapter_registry.py +26 -0
  18. vamp_cli/llm/debug_callbacks.py +18 -0
  19. vamp_cli/llm/router.py +247 -0
  20. vamp_cli/llm/usage.py +170 -0
  21. vamp_cli/logging_setup/logging_setup.py +37 -0
  22. vamp_cli/main.py +1023 -0
  23. vamp_cli/persistence/__init__.py +29 -0
  24. vamp_cli/persistence/compaction.py +372 -0
  25. vamp_cli/persistence/emitter.py +371 -0
  26. vamp_cli/persistence/event_log.py +303 -0
  27. vamp_cli/persistence/fork.py +81 -0
  28. vamp_cli/persistence/loader.py +616 -0
  29. vamp_cli/persistence/transcript.py +160 -0
  30. vamp_cli/prompts/prompts.py +182 -0
  31. vamp_cli/sandbox/__init__.py +0 -0
  32. vamp_cli/sandbox/base.py +22 -0
  33. vamp_cli/sandbox/config.py +16 -0
  34. vamp_cli/sandbox/docker.py +293 -0
  35. vamp_cli/sandbox/manager.py +31 -0
  36. vamp_cli/sandbox/result.py +14 -0
  37. vamp_cli/sandbox/terminal.py +151 -0
  38. vamp_cli/tools/__init__.py +0 -0
  39. vamp_cli/tools/base.py +44 -0
  40. vamp_cli/tools/diagnostic.py +120 -0
  41. vamp_cli/tools/execute_cmd.py +16 -0
  42. vamp_cli/tools/files.py +652 -0
  43. vamp_cli/tools/registry.py +163 -0
  44. vamp_cli/tools/tools_classes.py +622 -0
  45. vamp_cli/utils/__init__.py +0 -0
  46. vamp_cli/utils/error_codes.py +7 -0
  47. vamp_cli/utils/ignore.py +36 -0
  48. vamp_cli/utils/sha_256.py +32 -0
  49. vamp_cli/utils/token_tracker.py +31 -0
  50. vamp_cli/workspace/__init__.py +0 -0
  51. vamp_cli/workspace/workspace.py +16 -0
  52. vamp_cli-0.1.0.dist-info/METADATA +317 -0
  53. vamp_cli-0.1.0.dist-info/RECORD +57 -0
  54. vamp_cli-0.1.0.dist-info/WHEEL +5 -0
  55. vamp_cli-0.1.0.dist-info/entry_points.txt +2 -0
  56. vamp_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  57. vamp_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,133 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Sequence
5
+ from enum import Enum
6
+
7
+ from langchain_core.runnables import Runnable
8
+
9
+ from vamp_cli.config.schema import AppConfig, ProfileConfig
10
+ from vamp_cli.llm.router import ModelRouter
11
+ from vamp_cli.tools.registry import ToolRegistry
12
+ from vamp_cli.prompts.prompts import PromptStore
13
+ from vamp_cli.agent.roles import AgentRole
14
+ from vamp_cli.tools.base import Tools
15
+
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class AgentProfile:
20
+ role: str
21
+ model: Runnable[Any, Any]
22
+ tools: dict[str, Tools] # {name: Tools} - for execution dispatch
23
+ tool_schemas: list[dict] # OpenAI-format - for giving tool list to llm model
24
+ system_prompt: str
25
+ context_window: int
26
+
27
+
28
+ class AgentRuntime:
29
+
30
+ def __init__(
31
+ self,
32
+ config: AppConfig,
33
+ model_router: ModelRouter,
34
+ tool_registry: ToolRegistry,
35
+ prompt_store: PromptStore,
36
+ ) -> None:
37
+ self._config = config
38
+ self._model_router = model_router
39
+ self._tool_registry = tool_registry
40
+ self._prompt_store = prompt_store
41
+ self._profile_cache: dict[str, AgentProfile] = {}
42
+
43
+
44
+ def resolve(self, role: AgentRole) -> AgentProfile:
45
+
46
+ # check cache hit
47
+ if role.value in self._profile_cache:
48
+ return self._profile_cache[role.value]
49
+
50
+ profile = self._build_profile(role=role)
51
+
52
+ # cache the profile
53
+ self._profile_cache[role.value] = profile
54
+
55
+ return profile
56
+
57
+
58
+ def _build_profile(self, role: AgentRole) -> AgentProfile:
59
+
60
+ profile_cfg = self._resolve_profile(role)
61
+
62
+ # Tool objects, keyed by name - for execution.
63
+ tools_by_name = self._tool_registry.get_tools(profile_cfg.tools)
64
+
65
+ # OpenAI-format schemas - for binding into the router.
66
+ tool_schemas = self._tool_registry.get_openai_tools(profile_cfg.tools)
67
+
68
+
69
+ system_prompt = self._prompt_store.load(profile_cfg.prompt)
70
+
71
+ model = self._model_router.get(
72
+ profile_cfg.model,
73
+ tools=tool_schemas, # openai compatiable tool schema dict
74
+ usage_context=role.value,
75
+ )
76
+
77
+ context_window_size = self._config.models[profile_cfg.model].capabilities.context_window
78
+
79
+ profile = AgentProfile(
80
+ role=role.value,
81
+ model=model,
82
+ tools=tools_by_name,
83
+ tool_schemas=tool_schemas,
84
+ system_prompt=system_prompt,
85
+ context_window=context_window_size
86
+ )
87
+
88
+ return profile
89
+
90
+
91
+ def _resolve_profile(self, role: AgentRole)-> ProfileConfig:
92
+
93
+ profiles = self._config.profiles
94
+
95
+ if role.value in profiles:
96
+ return profiles[role.value]
97
+
98
+ if role.value not in profiles:
99
+ if role.value == "coder":
100
+ return profiles["default_coder"]
101
+ if role.value == "planner":
102
+ return profiles["default_planner"]
103
+
104
+ raise KeyError(
105
+ f"Role '{role}' is not configured and no 'default_{role.value}' profile exists"
106
+ )
107
+
108
+
109
+
110
+
111
+
112
+
113
+ '''
114
+ at call site:
115
+
116
+ tool_calls = response.tool_calls
117
+
118
+ for tc in tool_calls:
119
+
120
+ # openai raw tool call
121
+ name = tc["function"]["name"]
122
+ args = json.loads(tc["function"]["arguments"])
123
+
124
+ tool = profile.tools[name] # Dict[str, Tools]
125
+ result = tool.run(**args)
126
+
127
+
128
+ def normalize_tool_call(tc: dict) -> tuple[str, dict]:
129
+ if "function" in tc: # OpenAI raw
130
+ return tc["function"]["name"], json.loads(tc["function"]["arguments"])
131
+ return tc["name"], tc["args"] # LangChain
132
+
133
+ '''
@@ -0,0 +1,55 @@
1
+ from enum import Enum
2
+ import operator
3
+ from typing import TypedDict, Annotated, Literal
4
+
5
+ from langchain_core.messages import BaseMessage
6
+ from langgraph.graph.message import add_messages
7
+
8
+
9
+
10
+ class TodoItem(TypedDict):
11
+ id: str
12
+ description: str
13
+ status: Literal[
14
+ "pending",
15
+ "in_progress",
16
+ "completed",
17
+ "blocked",
18
+ ]
19
+
20
+
21
+ class AgentState(TypedDict):
22
+
23
+ # messages: Annotated[list[BaseMessage], add_messages]
24
+ planner_messages: Annotated[
25
+ list[BaseMessage],
26
+ add_messages,
27
+ ]
28
+
29
+ requires_execution: bool
30
+
31
+ coder_messages: Annotated[
32
+ list[BaseMessage],
33
+ add_messages,
34
+ ]
35
+
36
+ task: str
37
+
38
+ mode: Literal["normal", "plan"]
39
+
40
+ plan: str | None # only generated in plan mode
41
+
42
+ todo: list[TodoItem]
43
+
44
+ iteration: int
45
+
46
+ # tool-approval pipeline
47
+ _pending_tool_channel: str | None
48
+ _pending_tool_calls: list[dict]
49
+ _dangerous_tool_call_ids: list[str]
50
+ _approved_tool_call_ids: list[str]
51
+ _denied_tool_call_ids: list[str]
52
+
53
+
54
+
55
+
@@ -0,0 +1,17 @@
1
+ from enum import Enum
2
+ from typing import Optional
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class AgentStatus(str, Enum):
7
+ PLANNING = "planning"
8
+ EXECUTING = "executing"
9
+ VERIFYING = "verifying"
10
+ REPAIRING = "repairing"
11
+ COMPLETED = "completed"
12
+ FAILED = "failed"
13
+
14
+
15
+ class FeedbackResponse(BaseModel):
16
+ approved: bool
17
+ reason: Optional[str] = None
File without changes
@@ -0,0 +1,152 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ import yaml
8
+ from pydantic import ValidationError
9
+
10
+ from vamp_cli.config.schema import AppConfig, ModelConfig
11
+
12
+ DEFAULT_CONFIG_DIR = Path(__file__).resolve().parent
13
+ DEFAULT_CONFIG_PATH = DEFAULT_CONFIG_DIR / "default_config.yaml"
14
+
15
+ CONFIG_PATH = Path.home() / ".vamp/config.yaml"
16
+
17
+ class ConfigError(ValueError):
18
+ """Raised when configuration is structurally invalid or incomplete."""
19
+
20
+
21
+ def load_config() -> AppConfig:
22
+ """Load, validate, resolve secrets, and cross-check the YAML config.
23
+
24
+ This is the only place in the application that reads config.yaml.
25
+ """
26
+
27
+ config_path = CONFIG_PATH
28
+
29
+ if not config_path.exists():
30
+ config_path.parent.mkdir(parents=True, exist_ok=True)
31
+
32
+ with open(DEFAULT_CONFIG_PATH, "r", encoding="utf-8") as f:
33
+ content = f.read()
34
+
35
+ with open(config_path, "x", encoding="utf-8") as f:
36
+ f.write(content)
37
+
38
+
39
+ if not config_path.is_file():
40
+ raise ConfigError(f"Config file does not exist: {config_path}")
41
+
42
+ try:
43
+ with config_path.open("r", encoding="utf-8") as fh:
44
+ raw: Any = yaml.safe_load(fh) or {}
45
+ except yaml.YAMLError as exc:
46
+ raise ConfigError(f"Invalid YAML in {config_path}: {exc}") from exc
47
+ except OSError as exc:
48
+ raise ConfigError(f"Unable to read config {config_path}: {exc}") from exc
49
+
50
+ try:
51
+ config = AppConfig.model_validate(raw)
52
+ except ValidationError as exc:
53
+ raise ConfigError(f"Invalid configuration in {config_path}:\n{exc}") from exc
54
+
55
+ _resolve_api_keys(config)
56
+ _validate_references(config)
57
+ return config
58
+
59
+
60
+ def get_api_key(cfg: ModelConfig) -> str | None:
61
+ """Return a model's already-resolved API key."""
62
+
63
+ return cfg.api_key
64
+
65
+
66
+ def _resolve_api_keys(config: AppConfig) -> None:
67
+ """Resolve API key environment variables into the in-memory config."""
68
+
69
+ for name, cfg in config.models.items():
70
+ if not cfg.api_key_env:
71
+ continue
72
+ try:
73
+ value = os.getenv(cfg.api_key_env)
74
+ if not value:
75
+ # print(
76
+ # ConfigError(
77
+ # f"Model '{name}' requires environment variable "
78
+ # f"'{cfg.api_key_env}', but it is not set."
79
+ # )
80
+ # )
81
+ cfg.api_key = "EXAMPLEMODEL_API_KEY"
82
+ continue
83
+
84
+ cfg.api_key = value
85
+ except Exception:
86
+ pass
87
+
88
+
89
+
90
+ def _validate_references(config: AppConfig) -> None:
91
+ """Validate model references, profile references, and fallback cycles."""
92
+
93
+ model_names = set(config.models)
94
+
95
+ if not model_names:
96
+ raise ConfigError("At least one model must be configured.")
97
+
98
+ if config.profiles and any(p not in config.profiles for p in config.default_profile):
99
+ raise ConfigError(
100
+ f"default_profile '{config.default_profile}' is not defined in profiles."
101
+ )
102
+
103
+ for profile_name, profile in config.profiles.items():
104
+ if profile.model not in model_names:
105
+ raise ConfigError(
106
+ f"Profile '{profile_name}' references unknown model "
107
+ f"'{profile.model}'."
108
+ )
109
+
110
+ for model_name, model in config.models.items():
111
+ for fallback in model.fallbacks:
112
+ if fallback not in model_names:
113
+ raise ConfigError(
114
+ f"Model '{model_name}' references unknown fallback '{fallback}'."
115
+ )
116
+ if fallback == model_name:
117
+ raise ConfigError(
118
+ f"Model '{model_name}' cannot use itself as a fallback."
119
+ )
120
+
121
+ _check_fallback_cycles(config)
122
+
123
+
124
+ def _check_fallback_cycles(config: AppConfig) -> None:
125
+ visiting: set[str] = set()
126
+ visited: set[str] = set()
127
+ path: list[str] = [] # tracks the exact order of traversal
128
+
129
+ def visit(model_name: str) -> None:
130
+ if model_name in visiting:
131
+ cycle_start_idx = path.index(model_name)
132
+ cycle_path = path[cycle_start_idx:] + [model_name]
133
+ path_str = " -> ".join(cycle_path)
134
+ raise ConfigError(
135
+ f"Fallback cycle detected involving model '{model_name}'."
136
+ f"Fallback cycle: {path_str}"
137
+ )
138
+ if model_name in visited:
139
+ return
140
+
141
+ visiting.add(model_name)
142
+ path.append(model_name) # push to history stack
143
+
144
+ for fallback in config.models[model_name].fallbacks:
145
+ visit(fallback)
146
+
147
+ path.pop() # pop from history stack
148
+ visiting.remove(model_name)
149
+ visited.add(model_name)
150
+
151
+ for model_name in config.models:
152
+ visit(model_name)
@@ -0,0 +1,82 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
6
+
7
+
8
+ class ModelCapabilities(BaseModel):
9
+ """Declarative capabilities used by higher layers before invocation."""
10
+
11
+ model_config = ConfigDict(extra="allow")
12
+
13
+ supports_tools: bool = True
14
+ supports_parallel_tools: bool = True
15
+ supports_streaming: bool = True
16
+
17
+ supports_reasoning: bool = False
18
+ reasoning_param: str | None = None # ex: "reasoning_effort" or "thinking"
19
+ reasoning_value: Any | None = None # ex: "high" or {"type": "enabled", "budget_tokens": 4000}
20
+
21
+ context_window: int = 128_000
22
+
23
+
24
+ class ModelConfig(BaseModel):
25
+ """Configuration for one logical model name."""
26
+
27
+ model_config = ConfigDict(extra="forbid")
28
+
29
+ model: str
30
+ api_base: str | None = None
31
+ api_key_env: str | None = None
32
+
33
+ # Resolved by config.loader.py from api_key_env.
34
+ # It is excluded from normal serialization and displayed masked by SecretStr.
35
+ api_key: str | None = Field(default=None, repr=False)
36
+
37
+ params: dict[str, Any] = Field(default_factory=dict)
38
+ fallbacks: list[str] = Field(default_factory=list)
39
+ adapter: str = "default"
40
+ capabilities: ModelCapabilities = Field(default_factory=ModelCapabilities)
41
+
42
+
43
+ class ProfileConfig(BaseModel):
44
+ """Agent profile configuration consumed later by AgentRuntime."""
45
+
46
+ model_config = ConfigDict(extra="forbid")
47
+
48
+ model: str
49
+ tools: list[str] = Field(default_factory=list)
50
+ prompt: str
51
+
52
+
53
+ class AppConfig(BaseModel):
54
+ """Validated application configuration loaded from YAML."""
55
+
56
+ model_config = ConfigDict(extra="forbid")
57
+
58
+ models: dict[str, ModelConfig]
59
+ profiles: dict[str, ProfileConfig] = Field(default_factory=dict)
60
+ default_profile: list[str] = ["default_coder", "default_planner"]
61
+
62
+ @model_validator(mode="after")
63
+ def _check_references(self) -> "AppConfig":
64
+
65
+ # Ensure default_profile list is not empty
66
+ if not self.default_profile:
67
+ raise ValueError("config must define at least one item in default_profile")
68
+
69
+ # Check that every item in default_profile list actually exists under profiles
70
+ for p in self.default_profile:
71
+ if p not in self.profiles:
72
+ raise ValueError(f"default_profile item '{p}' is not defined in profiles.")
73
+
74
+ # Check profile models match defined models
75
+ for name, profile in self.profiles.items():
76
+ if profile.model not in self.models:
77
+ raise ValueError(
78
+ f"profile '{name}' uses unknown model '{profile.model}'. "
79
+ f"Defined models: {sorted(self.models)}"
80
+ )
81
+
82
+ return self
File without changes
@@ -0,0 +1,52 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Protocol
4
+
5
+ from langchain_core.runnables import Runnable
6
+
7
+ from vamp_cli.config.schema import ModelConfig
8
+
9
+
10
+
11
+ class ModelAdapter(Protocol):
12
+ def adapt(
13
+ self,
14
+ runnable: Runnable[Any, Any],
15
+ *,
16
+ model_name: str,
17
+ config: ModelConfig,
18
+ ) -> Runnable[Any, Any]:
19
+ ...
20
+
21
+
22
+ class PassThroughAdapter(ModelAdapter):
23
+ """Default adapter for models with no special handling."""
24
+
25
+ def adapt(
26
+ self,
27
+ runnable: Runnable[Any, Any],
28
+ *,
29
+ model_name: str,
30
+ config: ModelConfig,
31
+ ) -> Runnable[Any, Any]:
32
+ return runnable
33
+
34
+
35
+ class ReasoningAdapter(ModelAdapter):
36
+ """Injects provider-specific reasoning/thinking parameters into the runnable."""
37
+
38
+ def adapt(
39
+ self,
40
+ runnable: Runnable[Any, Any],
41
+ *,
42
+ model_name: str,
43
+ config: ModelConfig,
44
+ ) -> Runnable[Any, Any]:
45
+ caps = config.capabilities
46
+
47
+ # If the model configuration has reasoning enabled and specifies parameter structures
48
+ if caps.supports_reasoning and caps.reasoning_param:
49
+ kwargs = {caps.reasoning_param: caps.reasoning_value}
50
+ return runnable.bind(**kwargs)
51
+
52
+ return runnable
@@ -0,0 +1,26 @@
1
+ from vamp_cli.llm.adapter import (
2
+ ModelAdapter,
3
+ PassThroughAdapter,
4
+ ReasoningAdapter
5
+ )
6
+
7
+
8
+ class AdapterRegistry:
9
+ def __init__(
10
+ self,
11
+ adapters: dict[str, ModelAdapter] | None = None,
12
+ ) -> None:
13
+ self._adapters = {
14
+ "reasoning": ReasoningAdapter(),
15
+ "default": PassThroughAdapter(),
16
+ **(adapters or {}),
17
+ }
18
+
19
+ def get(self, name: str) -> ModelAdapter:
20
+ try:
21
+ return self._adapters[name]
22
+ except KeyError:
23
+ raise KeyError(
24
+ f"Unknown model adapter '{name}'. "
25
+ f"Available adapters: {sorted(self._adapters)}"
26
+ ) from None
@@ -0,0 +1,18 @@
1
+ import logging
2
+ import json
3
+ from langchain_core.callbacks import BaseCallbackHandler
4
+
5
+ log = logging.getLogger(__name__)
6
+
7
+ class DumpPayloadCallback(BaseCallbackHandler):
8
+ def on_llm_start(self, serialized, prompts, **kwargs):
9
+ log.info("=== LLM START ===")
10
+ log.info("serialized=%s", json.dumps(serialized, default=str)[:2000])
11
+ log.info("prompts=%s", prompts)
12
+
13
+ def on_chat_model_start(self, serialized, messages, **kwargs):
14
+ log.info("=== CHAT START ===")
15
+ log.info("messages=%s", messages)
16
+ log.info("invocation_params=%s", kwargs.get("invocation_params"))
17
+ log.info("options=%s", kwargs.get("options"))
18
+ log.info("metadata=%s", kwargs.get("metadata"))