Skip to content

Mark update/install packages commands as agent-invocable - #15137

Open
dhruvisompura wants to merge 15 commits into
mainfrom
dhruvi/agent-compatible-packages
Open

Mark update/install packages commands as agent-invocable#15137
dhruvisompura wants to merge 15 commits into
mainfrom
dhruvi/agent-compatible-packages

Conversation

@dhruvisompura

@dhruvisompura dhruvisompura commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Addresses part of #15063.

Summary

Makes the packages pane install and update commands invocable by Posit Assistant:

  • positronPackages.installPackage
  • positronPackages.updatePackage

The commands now take optional arguments for the package name and version. The packages pane UI doesn't provide either argument and shows the user a quick pick so they can provide both arguments.

For the agentic flow, we can now provide all arguments at once so the command can run without the interactive quickpicks showing up. We mark these arguments as "required" in the metadata we send to Assistant.

One difference between the user vs agentic flow is how we handle a scenario where a user says "install X" without specifying a version. The agent can now pass 'latest' in for the version for that case and on the command side we look up a real version number (should be the newest stable release but we should stress test this a bit).

I also fixed a bug where an error message notification was being mangled in the python extension: PyPI replies 404 with the plain text 404 Not Found for a package that does not exist, and we passed that straight to a JSON parser which failed to parse it.

Screenshots

Installing the newest version

packages-install-latest.mp4

A package that does not exist

packages-install-unknown.mp4

Release Notes

New Features

Bug Fixes

  • N/A

Validation Steps

@:packages-pane

  1. Verify the agent contract: run "Developer: Show Agent-Allowed Commands". Expected: both commands are listed, with name and version both required and a returns string on each.
  2. Verify user behavior is unchanged: in the Packages pane, install a package from the view-title menu, then update it from the row's Update button. Expected: the search and version pickers work as before.
  3. Verify the agent installs: with a Python session running, ask the assistant to "install cowsay". Expected: no picker, a progress notification, and the assistant reports the version it installed.
  4. Verify the agent updates: ask the assistant to "update cowsay to the latest version". Expected: no picker, and the assistant reports a concrete version rather than "latest". Use a pip environment, which is where this used to fail.
  5. Verify a failure is reported as a failure: ask the assistant to "install a package called asdasdadfasdf". Expected: nothing is installed, the assistant tells you it failed, and there is no JSON parse error.

@github-actions

Copy link
Copy Markdown

E2E Tests 🚀
This PR will run tests tagged with: @:critical @:packages-pane

Why these tags?
Tag Source
@:critical Always runs (required)
@:packages-pane PR description

More on automatic tags from changed files.

readme  valid tags

dhruvisompura and others added 13 commits July 28, 2026 14:40
sortVersionsDescending moves verbatim out of positronPackagesQuickPick
into a new packageVersions module, so a headless caller can reuse it
without depending on the quick-pick's IQuickInputService and Delayer.

Adds newestAvailableVersion, which picks the version to use when a
caller asks for the newest one. It prefers the newest stable release
over a newer prerelease, matching what the package managers install by
default: with 1.8, 1.9 and 2.0.0rc1 available, pip install <name>
installs 1.9, not the release candidate. Backends disagree on ordering
(R returns one version, PyPI ascending, conda newest-first), so the
list is sorted here rather than trusted.

Neither function was tested before; both are now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The agent contract these commands advertised did not hold. 'latest' was
mapped to an undefined version, but undefined is not universally
"newest": pip and uv-outside-a-project reject a missing version on
update outright, and on install they write a bare package name into a
requirements file with no --upgrade, so an already-installed package
resolves as satisfied and never moves. pip is the default manager for
Pyenv, Global, System and VirtualEnv environments, so an agent
following the documented contract failed on the common case.

'latest' is now resolved to a concrete version against the session's
repositories before the service is called, so every backend receives an
explicit target. This also fixes install's already-satisfied no-op.

Both commands now return a structured result instead of void. Every
failure path notified the user and returned void, which
validateAndExecute reports as success, so the assistant was told a
failed install succeeded. The two bugs compounded: an agent updating a
package to latest in a pip session failed inside the service, the error
went to a toast it could not see, and it reported success. The returns
metadata documents the shape.

The agent path never throws; the interactive path keeps its existing
rethrow, which only it can now reach. That distinction matters because
the Command Palette turns a thrown command error into a modal dialog.

Neither dispatch guard changes, so no existing caller is affected.
Install's guard is reverted to requiring both a name and a version,
restoring the version-picker fallback from #14608. 'latest' is the
agent's signal for "newest", and no human caller passes it, so argument
shape alone distinguishes the two callers. Correspondingly, version is
no longer marked optional in the agent metadata: omitting it installs
nothing and opens a picker that waits for a human. isOptional is
descriptive only, so this does not affect menu or UI callers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Covers what had no coverage: the dispatch decision and how an outcome
becomes a result. The two action classes are exported for this, matching
LookupHelpTopic; the quick-pick module is mocked because its real
MultiStepInput needs a live QuickInputService and a 300ms debounce, and
this change does not touch it.

The regression tests that matter are a failed install reporting
installed: false rather than success, 'latest' always reaching the
service as a concrete version, and a name-only invocation still opening
the picker.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
newestAvailableVersion skipped three-component prereleases like
2.0.0rc1 but not two-component ones like 1.0a2 and 2.0b1, or
dot-separated dev builds like 1.0.0.dev1. semver.valid rejects those
spellings and semver.coerce then drops the suffix, so they read as the
stable 1.0.0 and were installed in preference to an older stable
release. With 0.9 and 1.0a2 available it picked 1.0a2, where pip
install would give 0.9.

The version string is now checked directly as well as through semver. A
trailing digit group is required, which keeps out three things that only
look similar: conda's letter-suffixed builds, R's patch levels, and
post-releases, which are newer than the release they follow rather than
prereleases of it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
newestAvailableVersion reused the version picker's comparison, which
runs versions through semver.coerce and so truncates anything past
three segments. Four-segment versions tied and the tie fell back to
PyPI's ascending order, putting the older one first: with 1.2.3.4 and
1.2.3.5 available it picked 1.2.3.4. PEP 440 epochs were unparseable
for the same reason, so 3.0 beat 1!2.0.

Resolving a version automatically now uses its own comparison, which
keeps every numeric group and reads the epoch. R patch levels sort
correctly as a side effect (1.0-10 over 1.0-3), though R reports a
single version so nothing depended on it.

The picker keeps its existing ordering. A truncated sort is a cosmetic
problem when a human is choosing from the whole list, but silently
installs the wrong version when nothing is choosing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An agent can send the wrong shape: a missing version, a missing name,
a number where a string belongs, an empty string, or nothing at all.
None of those install or update anything unintended -- every one falls
through to the quick-pick and reports a false result. Pinned so that
stays true, along with extra trailing arguments being ignored.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The comment said an agent-compatible command must also be palette-exposed
so a user could run it themselves. Nothing enforces that: agentCompatible
alone decides what getAgentAllowedCommands returns, and a test covers a
command that is advertised without being in the palette.

Keeps the intent, which is still worth stating as a preference: putting an
agent-compatible command in the palette means anything an agent can do, a
user can do for themselves. Also stops naming f1 as the only route in --
the view focus commands reach the palette through a MenuId.CommandPalette
entry instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
searchPyPIVersions called response.json() without checking the response.
PyPI answers 404 for a project that does not exist with the plain-text
body '404 Not Found', which parses as the number 404 followed by
unexpected text, so the caller got 'Unexpected non-whitespace character
after JSON at position 4' instead of a usable answer.

A name that does not exist is routine, not exceptional -- any caller can
pass one that was typed or guessed -- so a 404 now returns no versions.
The packages commands already turn that into "No available versions of
'<name>' were found. Check the package name and the repositories
configured for this session." Other non-OK responses throw naming the
status, so a PyPI outage does not masquerade as a missing package.

The bug was unreachable until now: searchPackageVersions was only called
from the version quick-pick, which runs after the user has picked a real
package out of search results. Resolving 'latest' calls it with a name
supplied by an agent, which may not exist at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dhruvisompura
dhruvisompura force-pushed the dhruvi/agent-compatible-packages branch from 1854345 to 0c0b7c2 Compare July 28, 2026 23:45
The positron-python extension runs prettier --check in CI, and it wants
the throw on one line.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dhruvisompura
dhruvisompura marked this pull request as ready for review July 29, 2026 00:29
@dhruvisompura dhruvisompura changed the title Make packages pane install and update commands agent-invocable Mark update/install packages commands as agent-invocable Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant