Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1bdebbd
feat(acli): make rule 18 demand cross-reference resolution on transfo…
Sep 18, 2026
6008270
fix(cli): route the auth group to typer instead of the expert agent
Sep 20, 2026
afe9141
fix(cli): stop forwarding malformed commands into the expert agent
Sep 20, 2026
de16112
fix(cli): only bare dashscope and `expert` enter the agent
Sep 23, 2026
cd085a0
fix(acli): probe multimodal-generation first and remember the binding
Sep 23, 2026
7d28991
test(cli): pin dashscope auth whoami and login behaviour
Sep 23, 2026
6545692
acli: sync 0.6.7 — skills, search_files, platform line, Windows decoding
Sep 23, 2026
13baebd
test: silence pylint on the two new cli/acli test modules
Sep 23, 2026
474abb4
docs(acli): reconcile the bundled expert example with agenticCLI-exam…
Sep 24, 2026
db76344
fix(cli): point the non-TTY refusal at `dashscope expert "question"`
Sep 24, 2026
bf1c084
acli: sync the write_file temp-dir fix from agenticCLI 749999a
Sep 24, 2026
96bd10d
docs(acli): let the expert example answer questions about itself
Sep 24, 2026
528f57e
acli: sync the redaction and silence-nudge fixes from agenticCLI
Sep 24, 2026
c404dcc
acli: sync the memory/skill-loop stage-0+1 changes from agenticCLI
Sep 24, 2026
60caebc
acli: sync failed-turn tracing, /model and crypto tolerance from agen…
Oct 9, 2026
eb4cbdf
acli: sync thinking-budget, output-cap continuation and oneshot polic…
Oct 12, 2026
9e02261
acli: sync run_command budget back-pressure and 0.7.0 from agenticCLI
Oct 12, 2026
07929e1
Merge remote-tracking branch 'origin/main' into release/agentic-cli-7
Oct 12, 2026
ba38588
fix(acli): narrow _run_started for the repo mypy gate
Oct 12, 2026
d0ea611
acli: sync 0.7.1 from agenticCLI
Oct 14, 2026
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.7"
__version__ = "0.7.1"

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

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions dashscope/acli/cli/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@
"/tts",
"/camera",
"/provider",
"/model",
"/setup",
"/capability",
"/subagents",
Expand Down
7 changes: 7 additions & 0 deletions dashscope/acli/cli/dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,13 @@ def _handle_slash_command(

handle_provider_command(cmd, agent, config)
return True
elif cmd.startswith("/model"):
from dashscope.acli.cli.handlers_provider import (
handle_model_command,
)

handle_model_command(cmd, agent, config)
return True
elif cmd.startswith("/dev test provider") or cmd.startswith(
"/dev debug call",
):
Expand Down
54 changes: 54 additions & 0 deletions dashscope/acli/cli/handlers_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,3 +301,57 @@ def handle_provider_command(cmd: str, agent: Agent, config: Config) -> bool:
"[dim]/provider is now a Q&A wizard; args ignored[/dim]",
)
return _provider_wizard(agent, config)


def handle_model_command(cmd: str, agent: Agent, config: Config) -> bool:
"""/model [name] — switch the model inside the current provider.

Users coming from other CLIs type /model reflexively; making them
rediscover it under the /provider wizard costs a turn. Provider, key
and protocol stay with /provider.
"""
parts = cmd.split(maxsplit=1)
if len(parts) == 1 or not parts[1].strip():
console.print(
f"[cyan]{config.provider}/{config.model}[/cyan] "
f"protocol={config.protocol}",
)
console.print(
"[dim]Usage: /model <name> switches the model; "
"/provider covers provider/key/protocol.[/dim]",
)
return True

model = parts[1].strip()
normalized = normalize_model_name(model)
if normalized != model:
console.print(
f"[dim]Model name normalized to {normalized} "
f"(API model IDs are case-sensitive)[/dim]",
)

# Same rule as the wizard's model step: an unlisted name under a
# built-in provider is registered as a custom model; under an
# extension provider it is used as-is.
models = PROVIDER_MODELS.get(config.provider, [])
if not models:
ext = find_provider(config.provider)
if ext is not None:
models = ext.resolved_models()
if (
models
and normalized not in models
and config.provider in PROVIDER_MODELS
):
register_custom_model(config, config.provider, normalized)
console.print(
f"[green]✓ Registered {config.provider}/{normalized}[/green]",
)
config.model = normalized
config.save_global()
config.save_workspace()
if _rebuild_provider(agent, config):
console.print(
f"[green]✓ Switched: {config.provider}/{config.model}[/green]",
)
return True
9 changes: 9 additions & 0 deletions dashscope/acli/cli/repl.py
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,15 @@ async def _cli_confirm_callback(
continue
elif result:
continue
else:
# Not a registered command: the text goes to the model (an
# agent can legitimately answer slash-command questions),
# but say so — a typo'd command must not fail silently.
console.print(
f"[dim]Unknown command "
f"{user_input.split()[0]}; sending it to the model "
f"instead (/help lists commands).[/dim]",
)

# Expand @path tokens to file contents before sending to the agent.
# Done here (post slash-command handling) so slash commands themselves
Expand Down
69 changes: 68 additions & 1 deletion dashscope/acli/cli/runners.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,12 +148,32 @@ async def _run_oneshot(config: Config, prompt: str):
auto_approve=config.auto_approve,
confirm_mode=config.confirm_mode,
)

# Memory and the shared hook bus must exist before the Agent does (same
# wiring as cli/repl.py): without them oneshot had no memory provider at
# all, so every memory- or skill-shaped capability scored a constant zero
# in non-interactive runs.
memory = None
if config.memory_enabled:
from dashscope.acli.cli.handlers_profile import set_memory_client
from dashscope.acli.platforms import get_memory_provider

memory = get_memory_provider(config)
if memory:
set_memory_client(memory)

from dashscope.acli.hooks import create_hook_bus

hook_bus = create_hook_bus()

agent = Agent(
provider=provider,
executor=executor,
max_turns=config.max_turns,
memory=memory,
provider_name=config.provider,
model_name=config.model,
hook_bus=hook_bus,
oneshot=True,
)

Expand All @@ -178,11 +198,53 @@ async def _run_oneshot(config: Config, prompt: str):
set_delegate_parent(agent)
set_delegate_config(config)

from dashscope.acli.cli.mcp import _connect_mcp
from dashscope.acli.cli.mcp import (
_connect_mcp,
_disconnect_mcp,
_mcp_clients,
)
from dashscope.acli.tools.platform import register_platform_tools

register_platform_tools(config, connect_mcp_fn=_connect_mcp)

# The four registrations below mirror the interactive/TUI paths
# (cli/repl.py, _run_tui_mode). Oneshot used to skip them, which left
# skill packages unloaded and the session/skill/evolution tools
# unregistered in exactly the mode benchmarks run in.
from dashscope.acli.skills import get_skill_manager

skill_manager = get_skill_manager()
skill_manager._registry_url = getattr(config, "skill_registry", "")
skill_manager._global = False
skill_manager.load(hook_bus=hook_bus)

from dashscope.acli.tools.session import register_session_tools

register_session_tools(
config,
get_agent=lambda: agent,
get_provider_fn=get_provider,
connect_mcp_fn=_connect_mcp,
disconnect_mcp_fn=_disconnect_mcp,
list_mcp_services_fn=list_known_services,
get_mcp_clients_fn=lambda: _mcp_clients,
)

# Vision too: without it, oneshot (every bench run 071c–071k) offered
# no view_image at all — the prompt named a tool the model could not
# see — and read_file/run_command auto-perceive had no context.
from dashscope.acli.tools.vision import register_vision_tools

register_vision_tools(config, get_provider_fn=get_provider)

from dashscope.acli.tools.evolution import register_evolution_tools

register_evolution_tools(get_agent=lambda: agent)

from dashscope.acli.tools.skills import register_skill_tools

register_skill_tools(get_agent=lambda: agent)

expanded, images, audio_clips = _expand_at_references(prompt)
if images and not is_vision_model(config.model):
console.print(
Expand Down Expand Up @@ -225,7 +287,12 @@ async def _run_oneshot(config: Config, prompt: str):
if usage_path:
_write_usage_file(usage_path, executor)
if failure is not None:
from dashscope.acli.providers.hardening import account_error_hint

print(f"Error: {failure}", file=sys.stderr)
hint = account_error_hint(failure)
if hint:
print(hint, file=sys.stderr)
sys.exit(1)


Expand Down
11 changes: 10 additions & 1 deletion dashscope/acli/cli/streaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
AsyncSpinner,
UserAbortedTurn,
message_text_for_compress,
truncate,
)

console = Console()
Expand Down Expand Up @@ -206,7 +207,10 @@ async def _do_summarize(
lesson = f"Steps: {'; '.join(steps)}"

tracker.record_experience(
task_summary=task[:200],
# Flatten newlines and mark truncation: the task is usually
# the raw prompt, and a mid-word cut with embedded newlines
# breaks the numbered "Past Lessons" list in the system prompt.
task_summary=truncate(" ".join(task.split()), 200),
tools_used=tools_used,
outcome=outcome,
lesson=lesson,
Expand Down Expand Up @@ -313,6 +317,11 @@ def _sigint_handler():
console.print("\n[dim]Turn aborted[/dim]")
except Exception as e:
console.print(f"\n[red]Error: {e}[/red]")
from dashscope.acli.providers.hardening import account_error_hint

hint = account_error_hint(e)
if hint:
console.print(f"[yellow]{hint}[/yellow]")
finally:
# Restore default SIGINT handler
try:
Expand Down
5 changes: 5 additions & 0 deletions dashscope/acli/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@
"/provider",
"Configure Provider / Key / model / protocol (Q&A style)",
),
(
"/model",
"Switch the model within the current provider "
"(/model <name>)",
),
(
"/trust",
"Tool trust/deny cache for this session "
Expand Down
1 change: 1 addition & 0 deletions dashscope/acli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"qwen-max",
"qwen-plus",
"qwen-turbo",
"qwen3-vl-plus",
"qwen-vl-max",
"qwen-vl-plus",
"qwen-omni-turbo",
Expand Down
27 changes: 22 additions & 5 deletions dashscope/acli/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,8 +308,21 @@ def record_prompt_composition(
self,
messages: list[dict],
tools_schema: list[dict] | None = None,
) -> None:
"""Accumulate prompt size (chars) by category for one LLM call."""
) -> dict[str, int]:
"""Accumulate prompt size (chars) by category for one LLM call.

Also returns the per-call breakdown so the trace can persist what
this specific request was made of; the accumulator feeding /stats
keeps its coarser system/user/assistant/tools buckets.
"""
per_call = {
"system_chars": 0,
"user_chars": 0,
"assistant_chars": 0,
"tool_result_chars": 0,
"tool_call_args_chars": 0,
"tools_schema_chars": 0,
}
for msg in messages:
role = msg.get("role")
content = msg.get("content")
Expand All @@ -321,16 +334,20 @@ def record_prompt_composition(
n = 0
if role in ("system", "user", "assistant"):
self._prompt_composition[role] += n
per_call[f"{role}_chars"] += n
elif role == "tool":
self._prompt_composition["tools"] += n
per_call["tool_result_chars"] += n
if role == "assistant":
for tc in msg.get("tool_calls") or []:
args = (tc.get("function") or {}).get("arguments") or ""
self._prompt_composition["tools"] += len(args)
per_call["tool_call_args_chars"] += len(args)
if tools_schema:
self._prompt_composition["tools"] += len(
json.dumps(tools_schema, ensure_ascii=False),
)
n = len(json.dumps(tools_schema, ensure_ascii=False))
self._prompt_composition["tools"] += n
per_call["tools_schema_chars"] += n
return per_call

async def execute(self, tool_def: ToolDefinition, arguments: dict) -> str:
"""Run one tool call and return its text result.
Expand Down
Loading
Loading