Plot vision: the model can now see your plots.
When eval_r code draws a plot – base graphics or a returned
ggplot/lattice object – hal captures it as PNG and attaches it to the
tool result as an image, so the model can describe, critique, and
iterate on actual visual output (“make this plot publication-ready” now
works on what the plot looks like). Supported on the
vscode backend (requires the bundled hal-bridge 0.1.4)
and the claude backend (MCP image content blocks). The
copilot backend stays text-only until its CLI’s image
forwarding is verified. Disable with
hal_configure(plot_vision = FALSE). Notes: returned ggplot
objects are also printed to your device (they now appear in the Positron
plots pane); render errors surface to the model as a
## Plot render error section; plots written to file devices
the code opens itself are not echoed; one image per eval (the final
page); images larger than 5 MB are skipped.
hal-bridge 0.1.4 (bundled): tool results can
carry an image part (LanguageModelDataPart.image); if the
selected model rejects image input at runtime, the bridge strips images
and retries once text-only; request bodies are capped at 20 MB (413
beyond); minimum Positron/VS Code engine raised to 1.103 (first stable
release with the image API). The vscode client also prunes all but the
newest 2 images from the resent history so long sessions don’t re-bill
every old plot.
hal_do() now verifies its
transforms. After a successful pipe-mode transform, hal
compares input and output and prints a one-line structural report – row
deltas, columns added/removed, class changes, introduced NAs
(hal_do: 32 -> 14 rows | +1 col (kpl)). The full report
is attached as attr(result, "hal_verify") (print it for
details). Suspicious patterns – output identical to input, or 0-row
output – raise a classed hal_do_warning. Report-only by
design: deltas are often intentional (“filter mpg > 20”
should shrink the data), so verification never triggers retries
or changes values. Disable with .verify = FALSE or
hal_configure(verify = FALSE).
Fixed: the default model on the copilot and vscode
backends no longer exists. Both defaulted to
"claude-sonnet-4.6", which GitHub Copilot and
vscode.lm have since dropped from their catalogs — neither
backend serves that id any more. Both now default to
"claude-sonnet-5", verified present in both live catalogs.
The claude backend was unaffected: it passes the "sonnet"
alias, which the CLI resolves server-side and which cannot go stale this
way.
Fixed: stale model metadata. The Claude
backend’s alias descriptions still named Sonnet 4.6 and Opus 4.8 as the
current tiers (now Sonnet 5 and Opus 5), and reported a 200K context
window for all three aliases — Sonnet and Opus are 1M natively, and only
Haiku is 200K. Context windows for the newer
Copilot/vscode.lm ids (claude-opus-4.8,
claude-opus-5, claude-sonnet-5) were missing
and reported as NA. Documentation examples naming retired
ids were updated.
Missing Copilot? hal now points you at Claude if you have
it. When hal falls back to the copilot backend and can’t find
the Copilot CLI, but Claude Code is installed, the error,
hal_status(), and hal_setup() now say so and
give the one command that gets you working:
hal_configure(backend = "claude"). The install instructions
are still shown. Default backends are unchanged — this only changes the
message in a case that already failed. Detection checks the
PATH and CLAUDE_CLI_PATH only; it never runs a
subprocess.
Fixed: slow eval_r calls could abort a
healthy Claude turn. Tool calls execute synchronously inside
the Claude client’s response loop, but the prompt deadline only reset
when the CLI produced output – so time spent running your R code counted
as the model stalling. A single slow eval (eval_timeout
defaults to 30s, roughly double that when plot vision re-renders) could
exhaust the 60s prompt_timeout and abort a turn that was
progressing normally. Time spent servicing tool calls is now credited
back to the deadline.
Fixed: a failed Claude turn wedged the session.
The client marked a session as created only after a successful
turn. If a turn timed out or the CLI died mid-stream, the session
already existed in Claude Code’s store but hal did not know it – so
every subsequent prompt re-sent --session-id with a live
uuid and was rejected, until hal_reset(). The session is
now latched as soon as the CLI announces it, so a failed turn is simply
retried with --resume. Timeout messages say so.
Fixed: hal_setup(backend = "claude")
installed the Copilot CLI. The documented "claude"
value had no branch in hal_setup(), so it fell through to
the Copilot path and either mislabelled Claude as Copilot or ran
npm install -g @github/copilot. There is now a real Claude
branch that verifies the claude binary, reports its
version, and points at the download plus one-time OAuth step. It
deliberately does not auto-install: Claude Code’s sign-in is
interactive, and an npm install produces exactly the .cmd
shim whose stdio processx drops on Windows.
Fixed: MCP subprocess crash on R 4.1–4.3. The
generated MCP server script used %||% without defining it;
base R only gained %||% in 4.4.0. The script now defines
it, matching the package’s declared R >= 4.1 support.
Fixed: blank destination in the first-run data notice on the vscode backend. The one-time “hal sends prompts and tool results to the …” notice had no vscode branch and rendered an empty slot; it now names the model behind your Positron Copilot sign-in.
CRAN preparation.
hal() now asks before creating
hal.md. Previously it wrote the project memory
file into your working directory on first use and told you afterwards.
It now offers — “Create hal.md?”, default no — and writes only on an
explicit yes. It never asks in a non-interactive session, asks at most
once per folder per R session, and a decline is not undone by
hal_reset(). Turn the offer off with
options(hal.memory_prompt = FALSE). An existing
hal.md is still read in any context, whatever you
answered.hal_query_map surface: the
hal_configure(max_spawn =) parameter and
hal.max_spawn option documented a function that does not
exist.Rscript subprocesses)
are skipped on CRAN; the rest of the suite runs there.DESCRIPTION rewritten per CRAN conventions; stale
LICENSE copyright holder fixed; .vscode/
excluded from the build.excelR() is now
hal_excel(). Clean rename (no alias) to match the
hal_ prefix convention before anyone depends on it. The
result class is hal_excel_code and the verification report
attribute is attr(x, "hal_excel"). Functionality is
unchanged.
New hal_status(): one traffic-light
diagnostic for the whole stack. Reports the resolved backend
(and why), whether its transport is reachable, session state, and — when
something is wrong — the single next step to fix it. Returns a
structured list invisibly for programmatic use. This is the first thing
to run when hal misbehaves, and the thing to paste into a bug
report.
Positron setup no longer requires the GitHub
CLI. hal_setup() was still gating the vscode path
on gh being installed and gh auth status
passing — a leftover from before the bridge VSIX was bundled (0.1.1).
The gate is gone; the only external requirement (a Copilot sign-in
inside Positron itself) is now surfaced as a reminder in the next-step
bullets, since hal cannot verify it from R.
hal_do() now aborts on failure in
non-interactive contexts. Previously a failed generation warned
and passed .data through unchanged everywhere — in a script
or R Markdown pipeline that means silently continuing with untransformed
data. Interactive sessions keep the forgiving warn-and-passthrough;
scripts get an error. Override either way with
hal_configure(do_on_fail = "warn"|"abort").
Default hal_do() retries raised from 1 to
2 (hal.do_retries), matching the default-on
edit-in-place behavior: more self-correction before giving up.
hal_excel() inherits the same default.
hal failures now signal classed conditions.
hal() transport failures abort with
hal_transport_error in non-interactive contexts;
hal_do() signals hal_do_error /
hal_do_warning; hal_ask() signals
hal_ask_warning. All inherit from hal_error /
hal_warning, so programmatic callers can
tryCatch(..., hal_error = ...) instead of string-matching
messages.
Environment auto-detect is harder to
false-positive. hal()’s
use_env = NULL auto-detection no longer injects your
environment when a prompt merely contains English function words that
collide with object names (“show me all the columns
on that table” with objects
all/on). Backtick-quoting,
$/[ subsetting, or calling the object still
triggers injection deliberately. Data-science nouns (data,
model, fit, …) still match by name — missing
real context costs more than a small extra snapshot.
Website URL fixed. DESCRIPTION,
_pkgdown.yml, and the README pointed at
d-m4rk.github.io/hal (404); the site deploys at
arclite-red.github.io/hal.
hal-bridge discovery file moved out of the system temp
dir (bridge 0.1.2). The bridge previously wrote its port +
token to %TEMP%\hal-bridge.port, which the OS
garbage-collects (e.g. Windows Storage Sense). After a day or two the
file was swept while the bridge was still listening, so
hal_bridge_status() reported the bridge as gone and users
reinstalled needlessly. The discovery file now lives in a durable
per-user app-data dir — %LOCALAPPDATA%\hal-bridge\port.json
on Windows, $XDG_RUNTIME_DIR/hal-bridge/port.json (else
~/.cache/hal-bridge/port.json) on POSIX. Requires the
bundled bridge 0.1.2; older bridges write the old location and won’t be
found — reinstall with hal_install_bridge(force = TRUE) and
cold-restart Positron.
hal_config()$backend now reports the
resolved backend, not the raw option. Previously it read
getOption("hal.backend", "copilot") with a hardcoded
literal fallback, so in Positron it lied and said "copilot"
even though hal() would actually route to
"vscode". Now uses .hal_backend() — the same
resolver hal() uses — so the reporter and the router
agree.
Bridge install messaging now tells users to fully quit
Positron, not just “Reload Window”. On a fresh extension
install the extension host only loads new extensions on a cold start;
“Reload Window” is insufficient and leaves users stuck at
hal_bridge_status() reporting the bridge as not installed.
Updated in: hal_install_bridge() success message,
.hal_bridge_discover() not-found error,
hal_setup() next- step bullets, and the
backends vignette install snippet. Runtime reload prompts
(crashed bridge, 401 token rotation) still say “reload Positron” since
Reload Window is sufficient there.
hal_install_bridge() now installs from a
bundled VSIX. The hal-bridge extension (7 KB) ships in
inst/extdata/ and is installed directly into Positron — no
GitHub download, no auth token, no SHA pin. Removes the install-time
gh auth login / GITHUB_PAT requirement that
broke fresh installs on machines without a token.
local_path = "..." still works for testing dev builds;
version and verify arguments are gone (no
longer meaningful). To bump the bridge, ship a new hal release with the
updated VSIX in inst/extdata/.
Internal: deleted .hal_download_bridge_vsix,
.hal_github_token, .hal_github_api_get,
.hal_github_download_asset, .hal_sha256_file,
and the BRIDGE_SHA256 / BRIDGE_REPO constants.
Dropped digest and openssl from
Suggests.
Initial public release.
New vscode backend – talks to
Positron’s built-in vscode.lm API via a small localhost
HTTP bridge (the hal-bridge Positron extension). Skips the
Copilot CLI entirely: no Node.js, no @github/copilot
package, no --additional-mcp-config plumbing. Tool calls
round-trip directly through R, so eval_r runs without an
MCP subprocess.
hal_install_bridge() – downloads the pinned VSIX from
the hal-bridge private GitHub release and installs it into
Positron. GitHub auth comes from gh auth token, git
credential helper, or GITHUB_PAT (in that order); no
separate token setup needed for most users.hal_bridge_status() – ping/version/port
diagnostics.hal_setup() now detects Positron and walks through the
bridge install path automatically; pass backend = "copilot"
to force the legacy CLI flow.hal_models() queries the bridge’s /models
endpoint and returns whatever vscode.lm exposes to your
Copilot session.hal_available() is backend-aware: takes
backend = "..." to probe a specific transport without
changing the session default.permission_policy is now honored on the vscode backend
(previously accepted for API parity but silently always auto-allowed).
Supports "auto-allow" (default), "auto-deny",
"ask" (interactive prompt; denies in non-interactive
sessions), or a function receiving
list(backend = "vscode", tool_name, input) and returning a
string containing "allow" or "deny". Denied
calls are surfaced to the model as a tool error and recorded with
status = "denied".Smart-default backend – when
hal.backend is unset, hal now resolves to
"vscode" inside Positron (POSITRON_VERSION
set) and "copilot" everywhere else. Existing users with
options(hal.backend = "...") are unaffected.
BREAKING: Removed the blackboard API
(hal_bb_put(), hal_bb_get(),
hal_bb_list(), hal_bb_rm(),
hal_bb_clear(), hal_bb()). Replaced with
use_env parameter on hal() that auto-injects
the caller’s environment snapshot so the model uses eval_r
directly on your objects. No manual staging needed —
hal("analyze df", use_env = TRUE) just works.
BREAKING: Removed allow_eval
parameter from hal() and hal_ask().
eval_r tool is now always registered at session init. It is
inert without the use_env system prompt nudge.
write_r_script removed (redundant with the SDK’s built-in
create tool).
Bidirectional IPC: eval_r tool calls execute in the
user’s live R session via file-based message passing between the MCP
subprocess and parent R session. Model can inspect and modify caller
environment objects.
hal_do() retry mechanism: automatic retry on code
parse/execution failure, model self-corrects within the same disposable
session. Configurable via
hal_configure(do_retries = N).
Enhanced environment descriptions in hal_do()
standalone mode: data frame column names/types, function bodies for
small functions.
hal_configure() now accepts
permission_policy and session_quiet for
controlling the global session without using the R6 API.
hal_chat() and hal_client() wrappers
now accept permission_policy and quiet
directly.
hal_setup() – guided CLI installation and
authentication helper. Checks for Node.js, installs the Copilot CLI via
npm, and walks through copilot login.
Full functional API: hal(), hal_ask(),
hal_do(), hal_history(),
hal_reset().
Pipe-friendly verbs: hal_ask() for data-aware
analysis, hal_do() for code generation (pipe and standalone
modes).
Edit-in-place: hal_do() replaces itself in the
editor with generated code when running from an IDE script.
Configuration via hal_configure() /
hal_config() with hal.* options.
Custom tool calling via MCP: hal_tool(),
hal_register_tool(),
hal_register_package_tools(),
hal_register_tool_specs().
Governance layer: eval_r denylist (AST walker), credential scanner, eval timeout, spawn cap.
ANSI-colored output formatting with optional typewriter streaming.
High-level chat API via HalChat R6 class, mirroring
the ellmer::Chat interface.
Low-level ACP transport via HalClient R6 class
(NDJSON over stdio).
S3 data objects: hal_response,
hal_turn, hal_tool_call with print/format
methods.
Mid-session model switching via
$switch_model().
Session modes: $set_mode("agent") or
$set_mode("plan").
Cancel support: Ctrl+C interrupt + $cancel()
method.
Streaming callbacks: on_text,
on_tool_call, on_thought.
Permission handling for built-in tool calls
("auto-allow", "auto-deny", or custom
function).
hal_available() checks for a working Copilot CLI
installation.
hal_models() lists available models and usage
multipliers.
CLI discovery via standalone copilot binary,
gh copilot --, or COPILOT_CLI_PATH env
var.
Tool registration compatible with
ellmer::ToolDef.