Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dashscope/acli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import uuid

__version__ = "0.6.2"
__version__ = "0.6.4"

# Per-process identifier sent as x-dashscope-sdk-session-id so the
# backend can group multi-turn requests from one CLI run.
Expand Down
249 changes: 206 additions & 43 deletions dashscope/acli/agent.py

Large diffs are not rendered by default.

58 changes: 36 additions & 22 deletions dashscope/acli/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,33 @@
_scheduler = None


def _apply_cli_overrides(
config: Config,
protocol_override: str | None,
max_turns_override: str | None,
) -> None:
"""Apply --protocol/--max-turns to a loaded config; exit on bad input."""
if protocol_override:
if protocol_override.lower() in ("openai", "anthropic"):
config.protocol = protocol_override.lower()
else:
print(
f"Error: unknown protocol '{protocol_override}' "
f"(choices: openai, anthropic)",
)
sys.exit(1)

if max_turns_override is not None:
try:
config.max_turns = int(max_turns_override)
except ValueError:
print(
f"Error: --max-turns must be an integer, got "
f"'{max_turns_override}'",
)
sys.exit(1)


def main():
# Parse --cli / --tui / --dry-run flags (can appear anywhere in argv)
use_cli = "--cli" in sys.argv
Expand Down Expand Up @@ -146,7 +173,7 @@ def main():
)
print(
" --max-turns <n> Override max conversation "
"turns (default 1000)",
"turns (default from config: 50)",
)
print(
" --dry-run Preview current config (loaded "
Expand Down Expand Up @@ -175,6 +202,13 @@ def main():
print('Usage: acli -c "your request"')
sys.exit(1)
config = Config.load()
# This branch returns before main()'s override tail, so the
# flags must be applied here or oneshot silently ignores them.
_apply_cli_overrides(
config,
protocol_override,
max_turns_override,
)
asyncio.run(_run_oneshot(config, prompt))
return
elif arg in ("example", "examples"):
Expand All @@ -186,27 +220,7 @@ def main():
_mcp_server_main()
return
config = Config.load()
# Apply --protocol override
if protocol_override:
if protocol_override.lower() in ("openai", "anthropic"):
config.protocol = protocol_override.lower()
else:
print(
f"Error: unknown protocol '{protocol_override}' "
f"(choices: openai, anthropic)",
)
sys.exit(1)

# Apply --max-turns override
if max_turns_override is not None:
try:
config.max_turns = int(max_turns_override)
except ValueError:
print(
f"Error: --max-turns must be an integer, got "
f"'{max_turns_override}'",
)
sys.exit(1)
_apply_cli_overrides(config, protocol_override, max_turns_override)

# Handle --dry-run: preview configuration without starting the agent
if use_dry_run:
Expand Down
4 changes: 2 additions & 2 deletions dashscope/acli/cli/handlers_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ def _setup_finalize(config: Config, agent) -> None:

def _apply_preset_bailian(config: Config) -> None:
config.provider = "tongyi"
config.model = "qwen3.7-plus"
config.model = "qwen3.8-max"
config.enabled_capabilities = [
"bailian.mcp",
"bailian.cli",
Expand Down Expand Up @@ -284,7 +284,7 @@ async def _handle_setup(config: Config, agent) -> None:
console.print("\nSelect a configuration mode:")
console.print(
" [cyan][1][/cyan] [bold]Alibaba/Bailian[/bold] (default) — "
"tongyi/qwen3.7-plus + bailian.mcp/cli",
"tongyi/qwen3.8-max + bailian.mcp/cli",
)
console.print(
" [cyan][2][/cyan] China general — "
Expand Down
7 changes: 5 additions & 2 deletions dashscope/acli/cli/repl.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,10 @@ async def _run_loop(config: Config):
sync_extensions_into_catalog(_ext)
# _load_plugins() — deprecated; plugins load via the hooks mechanism
provider = get_provider_chain(config)
executor = Executor(auto_approve=config.auto_approve)
executor = Executor(
auto_approve=config.auto_approve,
confirm_mode=config.confirm_mode,
)

# Initialize memory
# Memory client managed by handlers_profile
Expand Down Expand Up @@ -248,7 +251,7 @@ async def _run_loop(config: Config):
if not WORKSPACE_CONFIG_FILE.exists():
has_api_key = bool(config.api_key)
using_defaults = (
config.provider == "tongyi" and config.model == "qwen3.7-plus"
config.provider == "tongyi" and config.model == "qwen3.8-max"
)
if not has_api_key or using_defaults:
await _handle_setup(config, agent)
Expand Down
11 changes: 9 additions & 2 deletions dashscope/acli/cli/runners.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,17 @@ async def _run_oneshot(config: Config, prompt: str):
sys.exit(1)

provider = get_provider_chain(config)
executor = Executor()
executor = Executor(
auto_approve=config.auto_approve,
confirm_mode=config.confirm_mode,
)
agent = Agent(
provider=provider,
executor=executor,
max_turns=config.max_turns,
provider_name=config.provider,
model_name=config.model,
oneshot=True,
)

# Pin parent agent ref for local.subagent / local.delegate BEFORE platform
Expand Down Expand Up @@ -278,7 +282,10 @@ def _run_tui_mode(config: Config):
# CLI mode uses the full provider chain so temporary failures can fall back
# to configured alternatives instead of dying immediately.
provider = get_provider_chain(config)
executor = Executor(auto_approve=config.auto_approve)
executor = Executor(
auto_approve=config.auto_approve,
confirm_mode=config.confirm_mode,
)

from dashscope.acli.agents.delegate import (
set_config as set_delegate_config,
Expand Down
31 changes: 26 additions & 5 deletions dashscope/acli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

PROVIDER_MODELS = {
"tongyi": [
"qwen3.8-max",
"qwen3.7-max",
"qwen3.7-plus",
"qwen3.5-plus",
Expand Down Expand Up @@ -226,7 +227,7 @@ def from_dict(cls, d: dict) -> DelegationConfig:
@dataclass
class Config:
provider: str = "tongyi"
model: str = "qwen3.7-plus"
model: str = "qwen3.8-max"
# Dual-LLM: when thinking_model is set, Plan/Thinking loop phases route to
# the thinking model; Execute uses the execution model (provider/model
# above).
Expand All @@ -241,6 +242,10 @@ class Config:
openai_api_key: str = ""
base_url: str = ""
auto_approve: bool = False
# Which tool executions prompt for confirmation:
# "all" - CONFIRM and DANGEROUS both prompt (classic)
# "dangerous" - only DANGEROUS prompts; CONFIRM auto-passes
confirm_mode: str = "dangerous"
max_turns: int = 50
timeout: int = 30
mcp_servers: list[MCPServerConfig] = field(default_factory=list)
Expand Down Expand Up @@ -277,9 +282,11 @@ class Config:
protocol: str = "openai" # "openai" | "anthropic"
tui: bool = True
# False = let the terminal handle the mouse (native selection).
# Default off on Windows: Textual's mouse-capture path is crash-prone
# in PowerShell/conhost; native terminal mouse still allows selection.
tui_mouse: bool = field(default_factory=lambda: os.name != "nt")
# Capture is on everywhere, including Windows: without it the wheel goes
# to the console host, which scrolls the whole alternate screen — the
# input box moves with the output and repaints slowly. Set false only if
# your terminal mishandles Textual's mouse reporting.
tui_mouse: bool = True
privacy_mode: bool = (
False # When True, all data stays local, no cloud capabilities
)
Expand Down Expand Up @@ -314,7 +321,7 @@ def api_key(self, value: str) -> None:
def load(
cls,
default_provider: str = "tongyi",
default_model: str = "qwen3.7-plus",
default_model: str = "qwen3.8-max",
) -> Config:
config = cls()
# Caller's defaults become the initial values; config files and env
Expand Down Expand Up @@ -542,6 +549,20 @@ def _load_workspace_from(self, path: Path):
self.max_turns = int(data["max_turns"])
except (ValueError, TypeError):
pass
if "auto_approve" in data:
val = str(data["auto_approve"]).lower()
self.auto_approve = val in ("true", "1", "yes")
if "confirm_mode" in data:
val = str(data["confirm_mode"]).lower()
if val in ("all", "dangerous"):
self.confirm_mode = val
# Env override for headless/benchmark use
if os.environ.get("ACLI_AUTO_APPROVE", "").lower() in (
"1",
"true",
"yes",
):
self.auto_approve = True
if "session_persist" in data:
val = str(data["session_persist"]).lower()
self.session_persist = val not in ("false", "0", "no")
Expand Down
2 changes: 1 addition & 1 deletion dashscope/acli/examples/basic-chat/.acli/config.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
user_name = "dashscope"
provider = "tongyi"
model = "qwen3.7-max"
model = "qwen3.8-max"
memory_user_id = "acli-basic"
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@
name = "tongyi"
base_url = "https://dashscope.aliyuncs.com/compatible-mode/v1"
api_key_env = "DASHSCOPE_API_KEY"
default_model = "qwen3.7-max"
default_model = "qwen3.8-max"
models = [
"qwen3.8-max",
"qwen3.7-max",
"qwen3.7-plus",
"qwen-turbo",
Expand Down
6 changes: 3 additions & 3 deletions dashscope/acli/examples/basic-chat/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ Declares which LLM providers acli can use. A minimal config needs just one `[[pr
name = "tongyi"
base_url = "https://dashscope.aliyuncs.com/compatible-mode/v1"
api_key_env = "DASHSCOPE_API_KEY" # ← stores only the env var name; the shell provides sk-xxx
default_model = "qwen3.7-max"
models = ["qwen3.7-max", "qwen3.7-plus", "qwen-turbo", "qwen-vl-max"]
default_model = "qwen3.8-max"
models = ["qwen3.8-max", "qwen3.7-max", "qwen3.7-plus", "qwen-turbo", "qwen-vl-max"]
vision_models = ["qwen-vl-max"] # ← tells acli these models accept image input
protocol = "openai" # ← openai / anthropic / dashscope
```
Expand Down Expand Up @@ -95,7 +95,7 @@ How to invoke:
```toml
user_name = "dashscope"
provider = "tongyi"
model = "qwen3.7-max"
model = "qwen3.8-max"
memory_user_id = "acli-basic"
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
name = "tongyi"
base_url = "https://dashscope.aliyuncs.com/compatible-mode/v1"
api_key_env = "DASHSCOPE_API_KEY"
default_model = "qwen3.7-plus"
default_model = "qwen3.8-max"
models = [
"qwen3.8-max",
"qwen3.7-max",
Expand Down
2 changes: 1 addition & 1 deletion dashscope/acli/examples/dashscope-sdk-expert/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ Before generating code, first verify the user's installed SDK version and API si
```toml
user_name = "dashscope"
provider = "tongyi"
model = "qwen3.7-plus"
model = "qwen3.8-max"
memory_user_id = "acli-dashscope"
```

Expand Down
26 changes: 25 additions & 1 deletion dashscope/acli/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,15 @@


class Executor:
def __init__(self, auto_approve: bool = False):
def __init__(
self,
auto_approve: bool = False,
confirm_mode: str = "dangerous",
):
self.auto_approve = auto_approve
# "all": CONFIRM and DANGEROUS both prompt; "dangerous": only
# DANGEROUS prompts (CONFIRM-level tools auto-pass).
self.confirm_mode = confirm_mode
# Trust cache scoped to ONE conversation turn (a single user prompt
# plus the agent loop that answers it). Agent.run / run_stream clear
# these in a finally block on completion or abort. DANGEROUS tools
Expand Down Expand Up @@ -214,6 +221,14 @@ async def _async_check_permission(
and policy.check_command(cmd) == "deny"
):
return False
# confirm_mode="dangerous": only risky (DANGEROUS) operations
# prompt; CONFIRM-level tools auto-pass. Policy deny rules
# above have already been honored.
if (
self.confirm_mode == "dangerous"
and tool_def.permission == PermissionLevel.CONFIRM
):
return True
# Auto-pass read-only shell commands
if tool_def.name == "run_command":
cmd = arguments.get("command", "")
Expand Down Expand Up @@ -284,6 +299,15 @@ def _check_permission(
console.print("[dim red]✗ command denied by policy[/dim red]")
return False

# confirm_mode="dangerous": only risky (DANGEROUS) operations
# prompt; CONFIRM-level tools auto-pass. Policy deny rules
# above have already been honored.
if (
self.confirm_mode == "dangerous"
and tool_def.permission == PermissionLevel.CONFIRM
):
return True

# Auto-pass read-only shell commands (grep, ls, find, git status, …).
# The classifier lives in shell.py since it owns the shell semantics;
# see is_safe_readonly for the allow/deny rules. Anything not
Expand Down
7 changes: 6 additions & 1 deletion dashscope/acli/memory/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@

from dashscope.acli.memory.experience import ExperienceTracker
from dashscope.acli.memory.plan import PlanTracker
from dashscope.acli.memory.reflection import ReflectionTracker
from dashscope.acli.memory.reflection import (
ReflectionTracker,
StagnationTracker,
)
from dashscope.acli.memory.tool_chains import ToolChainLibrary
from dashscope.acli.memory.trace import TraceLogger

Expand All @@ -38,12 +41,14 @@ def __init__(self, workspace_dir: Path):

self.plan = PlanTracker()
self.reflection = ReflectionTracker()
self.stagnation = StagnationTracker()
self.tool_chains = ToolChainLibrary(self.session_dir)

def reset(self) -> None:
"""Reset session memory (called at session start)."""
self.plan.clear_plan()
self.reflection.reset()
self.stagnation.reset()


class PersistentMemory:
Expand Down
Loading
Loading