Changelog
Source:NEWS.md
codeagent 0.2.3
This patch release adds optional Liquid Glass theming, fixes tool-card behavior, and closes security/integration blockers found during PR review. It includes one intentional safe-default change: team_lead() now defaults to "dont_ask".
Security and reliability
- Made matching explicit deny rules absolute across capability allows, per-tool overrides, modes, and read fast paths; initial plan sessions can no longer invoke
ExitPlanModewithout a trusted in-sessionEnterPlanModetransition. - Replaced sandboxed Bash
system2()execution withprocessxargv execution and a full replacement environment, confined Glob patterns and symlink results, and made the Data Shield portable policy block every non-delegated exec tool—including path-declaredLint, whose project.lintrmay execute R code—until a real OS sandbox is available. - Made PreToolUse installation and runtime exceptions fail closed, and rebuilt delegation guidance from the final live tool registry so missing/failed Agent or TeamRun registration is never advertised.
- Preserved dynamic plan-mode state when a Shiny permission/tool refresh rolls back.
Behavior changes
- Changed
team_lead()’s defaultpermission_modefrom"bypass"to"dont_ask". Existing trusted write workflows must now opt in explicitly to a more permissive mode; read-only review workflows continue to work with the safer default.
Shiny UI
- Replaced the built-in glassmorphism imitation with optional, exact-pinned
shinyglassmaterial rendering.codeagent_theme("glass", ...)now forwards Liquid Glass controls such aspreset,intensity,tint, andspecularto shinyglass, while codeagent owns only a thin shinychat header/sidebar/drawer/composer token adapter and dark-mode bridge. - Changed artifact-backed tool cards to stay collapsed after successful completion regardless of output length; error cards still open by default. This prevents normal long Read, Grep, and
use_skillresults from making the interface appear permanently expanded.
codeagent 0.2.2
This backward-compatible release adds adaptive request-boundary compaction, adopts ellmer’s request lifecycle hooks, and expands the Shiny theme system while preserving public interfaces.
Core
Reworked request-boundary compaction into an adaptive, model-aware pipeline: cheap result replacement and micro snip run before structured fresh recount; incremental/full summaries run only when still needed and are post-validated. Summary input now preserves tool request/result structure, PTL recovery drops pair-safe complete rounds, and compaction lifecycle hooks expose metadata only.
Updated the pinned ellmer development build to official
mainSHA2e96ac58a33d74bea585727daf8cd1535c67d7f1, which includes the mergedChat$on_request_start()/Chat$on_request_end()callbacks from tidyverse/ellmer#1052.Migrated mid-loop compaction from the per-tool
on_tool_resultworkaround toon_request_start, so context is checked before every model request. Threshold accounting now includes the complete outgoing turns, including a pending tool-result turn, while history rewrites continue to use ellmer’s supportedget_turns()/set_turns()contract.Adapted verified name-only model switching to ellmer’s new
Modelownership: deprecated Provider model fields are excluded from provider identity checks, whileModelparams and extra arguments remain strictly compared before an in-place switch.
Shiny UI
- Fixed the runnable theme/page-chat examples for Workbench and RStudio: they now preserve the
shiny.launch.browserproxy launcher and bind to0.0.0.0when Workbench URL environment markers are present, while ordinary environments remain loopback-only. Previously the proxy could open a blank page because the app listened only on127.0.0.1. - Added the independently selectable
theme = "aurora": static blue-indigo-purple ambient light with selective frosted sidebars, drawers, and composer controls, while chat, code, tool-result, table, and artifact surfaces remain high-opacity for sustained readability. Includes dark-mode, reduced-transparency, reduced-motion, and no-backdrop-filterfallbacks. - Unified the classic and
page_chatlayouts on shinychat’s officialpage_chat_theme()foundation. Added exportedcodeagent_theme(), built-intheme = "ios", and direct pass-through of custom bslib themes (includingshinychat::page_chat_theme(...)). - Refined the iOS preset with a Young Voice-style grouped canvas, compact 14px white cards, lightweight borders and shadows, white sidebars, and a bottom-anchored composer beside the voice/file footer actions.
- Refreshed artifact/tool-result cards with bslib white surfaces and shinychat-aligned Atom One syntax colors, replacing Prism’s legacy gray code background and text shadow.
codeagent 0.2.1
This is a backward-compatible release focused on portable tool results, current upstream integrations, Shiny presentation, safety, and documentation.
Portable tool results
- Added a versioned, UI-neutral tool-result artifact v1 contract (
codeagent.tool-artifact) with publictool_result_artifact()andtool_result_value()accessors. Streaming callbacks now exposeartifact, the optional official shinychatdisplayadapter, and the portablevaluefallback, allowing non-shinychat hosts to render native components without parsing shinychat HTML. - Preserved the official shinychat presentation path, including framed rich cards, while making malformed or unsupported artifact metadata fail soft. See
vignette("tool-artifacts")for the schema, version negotiation, security boundary, and migration guide.
Shiny and upstream integration
- Added the opt-in full-window
page_chatlayout with a 100%-width main chat, officialpage_chat_theme()baseline, persistent dark-mode and Workspace toolbar, resizable Output / Files / File drawer, attachment staging, and official shinychat tool displays. The classic layout retains its embedded width and disables unused native drawer/history presentation. - Improved greeting, slash-command, tool-display, session replay, file preview, and atomic settings/tool-group behavior while preserving codeagent ownership of streaming, permissions, sessions, and Data Shield.
- Added opt-in support for ellmer provider-native
ContentCitation/WebSourcecontent. Native citations are converted to opaque current-turn server references and rebuilt through the same fixed<shiny-aside>allowlist as custom WebSearch/WebFetch citations; provider and model markup is never trusted directly. - Citation claims, grounded spans, titles, quotes, and URLs pass through Data Shield and the public-URL policy. Citation mode remains buffer-then-show, rejects unsafe or prior-turn references, supports multiple sources per grounded span, and safely rebuilds lossless session replay.
- Re-pinned ellmer to GitHub HEAD
a64f94e644718c0598b01b0cd50a3c21c2646435, including refreshed bundled model/pricing data and Bedrock API mappings, without upgrading unrelated CRAN packages. - Unified the MCP client, server, child-process guard, tests, and documentation on
mcptools >= 1.0.2.9000; the architecture documentation now reflects Streamable HTTP, static headers, and OAuth discovery, PKCE, and token-refresh support.
Safety and compatibility
- Hardened Data Shield input, tool, and output boundaries, fail-closed handling, sandbox and network policy, reviewer isolation, audit metadata, and protected-data schema/context behavior.
- Shiny Agents discovery now supports project and user
.btw/agents/*.mddirectories alongside Claude and legacy.btw/agent-*.mdlocations, excludes.btw/btw.md, and honors YAMLnamemetadata. - Fixed the WEAR report tool to register under the stable
GenerateReportname expected by the built-in/reportskill. - Updated CLI, session, and tooling behavior for compatibility with the pinned ellmer, btw, shinychat, bslib, and Shiny development builds.
codeagent 0.2.0
-
Data Shield security hardening (second-round audit): 16 boundary fixes, all fail-closed. A follow-up security review found several paths where the shield could silently fail open; every one is now closed:
- Input/output gates fail closed. A scan exception, an unknown (typo’d) scanner name, an already-closed shield, or an unextractable attachment no longer passes the raw text through — the gates now block/redact, matching the tool-side ingress (which was already fail-closed).
-
Full turn-boundary coverage. The one-shot
codeagent()and the non-streaming REPL path now run both the input and output gates;agent_loopfalls back to the settings hook registry when none is passed. - Streaming output gate really redacts. When a shield is active, the Shiny and CLI streaming paths buffer the reply, scan it, then show the (possibly redacted) text once — instead of streaming plaintext to the screen and appending a warning after the fact. Without a shield, streaming is unchanged.
-
Tool boundary sees nested args and async results.
scan_tool_argsrecurses into nested list string leaves; the egress wrapper awaits a promise-returning tool and scans the resolved value. -
egress = "none"is a real deny, not a silent pass; the reviewer rail is reachable (review_code_public());refresh_data_shield_context()works for a bareChat; tool re-registration re-installs the shield wrapper; a rewritten PreToolUse input is re-checked against the gate authority. -
Code audit precision + read safety. The static source-reference audit only treats a literal path argument as static (nested
file.path()/paste0()/ a variable are flagged dynamic); binary R-data blobs are not text-audited; files are read with a byte cap and a TOCTOU re-check. -
Lifecycle & isolation. Reviewer isolation is verified (not best-effort);
close()clears strategies, both pipelines, and custom-scanner closures, with an R6 finalizer backstop and a Shiny session-end close; the no-network sandbox marks (and warns once) when OS isolation is unavailable. -
Compatibility shims.
HookEvent$USER_MESSAGEandrun_user_message()are restored as deprecated aliases;register_data()errors clearly ifcolumn_accessis passed in an old positional slot.
Bash sandbox gains OS-level no-network isolation via unprivileged namespaces. When the Bash sandbox runs with
allow_network = FALSEandunshareis available, commands are wrapped inunshare -Urn(a user+network namespace with no interface), so anyconnect()/socket()fails at the kernel level regardless of how the command is written — a bounded syscall boundary, not a bypassable command blacklist (e.g./dev/tcp/..., which thecurl/wgetblacklist never catches, is now blocked). The blacklist is kept as a cheap first line; whenunshareis unavailable the sandbox degrades to it. Network-allowed sandboxes and RunR are unchanged (RunR keeps callr + blacklist for now — wrapping callr’s internal fork inunshareis deferred).PreToolUse hooks can now rewrite a tool’s arguments before it runs (aligns with the Claude Agent SDK’s
updatedInput). APreToolUsehook that returnslist(action = "updated_input", input = <new args>)changes what the tool executes with;list(action = "deny", ...)rejects the call. Because ellmer’son_tool_requestis rejectable-only (a callback cannot rewrite the request), the rewrite happens one layer in: each tool is wrapped (.wrap_tool_pre_hook) so the hook runs inside the tool function, after the permission gate. Ordering isgate -> wrapper -> original, so a rewrite can never bypass permission checks (the gate always sees the original args), and the tool’s JSON schema is unaffected (ellmer derives it from the@argumentsslot, not the wrapped function’s formals). Zero ellmer patching.Data Shield redacts protected values inside tool arguments (ingress rewrite, symmetric to the existing egress scan).
DataShield$scan_tool_args()scrubs registered values / PII from each string argument using the same detectors asscan_prompt; a value pasted into a tool argument is redacted rather than the whole call being blocked. Runs in.data_shield_wrap_toolafter the permission gate.Data Shield now injects protected-dataset schemas into the system prompt (querychat-style ambient visibility). When a shield is active, every registered dataset’s filtered schema (the same output
DescribeDataproduces – identifier values suppressed, rare categories hidden) is placed in a<protected-data>block in the system prompt, so the model knows what protected data exists and its column structure without having to callDescribeDatafirst (previously it would fabricate column names/dims when it didn’t call the tool). TheDescribeDatatool is retained as the live, on-demand fallback. Behavior change: existing shielded clients now get a longer system prompt. Data registered before the client is built is in the initial prompt automatically; for data registered/uploaded at runtime, call the new exportedrefresh_data_shield_context(client)afterregister_data()(e.g. in a Shiny upload handler) to rebuild the system prompt with the new dataset – this preserves conversation history and only costs a one-time prompt-cache miss.register_data()deliberately does not auto-refresh, keeping the shield decoupled from the Chat. Scope note: this is edge-1 visibility only; switching a dataset’s security mode mid-conversation is a non-goal (the context window is immutable – the correct reset is a fresh conversation).Output gate (edge 3): Data Shield now scans the model’s final reply before it reaches the user. Data Shield previously guarded the two edges into the model (edge 1 input gate, edge 2 tool gate) but not the reply out to the user – so a model that reproduced a protected value it inferred from tool output (an aggregate edge 2 let through) leaked it even when the user’s own input was clean. The output gate (
R/output_gate.R.output_gate_scan()+DataShield$scan_response()) closes this, reusing the same value_match + PII detectors as the input gate (audited underedge = "response"). CLI (agent_loop) is non-streaming and redacts the reply in place; when a shield is active the Shiny and CLI streaming paths now buffer the reply, scan it, then show the (possibly redacted) text once – nothing reaches the browser until the output gate has run (no plaintext-then-warning). Configurable viasettings$data_shield_response_on_fail(redact/block/ask) andsettings$data_shield_output_scanners. This brings Data Shield to the canonical three-edge coverage (input / tool / output), matching the output rail of NeMo Guardrails / the Output Guard of Guardrails AI.Input and output gates take a configurable scanner list.
settings$data_shield_input_scanners/data_shield_output_scanners(defaultc("value_match", "regex")– secure-by-default) let a host drop a detector, e.g.c("value_match")to keep registered-value matching but skip PII regex. Backed by a newscanners=argument onDataShield$scan_prompt()(default runs both, fully backward compatible).Input gate (edge 1): Data Shield now scans all user input before it reaches the model. Previously Data Shield only guarded tool traffic (edge 2:
scan_ingress/scan_egress); the user’s own message went to the model unscanned. The input gate (R/input_gate.R.input_gate_scan()) adds edge-1 protection over all input: typed text and text-bearing attachments are scanned viaDataShield$scan_prompt()(registered protected value pasted in via value_match O(1) hash lookup + PII/token shapes via regex), and by default it redacts only the matched spans while keeping the rest of the user’s text (on_fail = "redact"/"block"/"ask"). Image attachments are a blind spot by default; an optionaldata_shield_image_scannerhook (defaultNULL) lets a host inject an OCR/VLM scanner. Aon_progresscallback lets a UI show “scanning data safety…”. Wired at the UserPromptSubmit point of both entry paths — the agent loop (CLI) and the Shiny stream. Two systems stay separate: the UserPromptSubmit hook may block/append but never redacts (CC parity); the Data Shield input gate may redact (its confidentiality job).data_shield_ocr_scanner(): an opt-in OCR image scanner for the input gate. Closes the image blind spot for text baked into screenshots: it OCRs the attachment with the optional package (aSuggestsdep), then runs the extracted text throughscan_prompt(), blocking the turn on a protected-value hit. Opt-in (scheme A) — the defaultdata_shield_image_scannerstaysNULL; wire it explicitly viasettings$data_shield_image_scanner = data_shield_ocr_scanner(shield). When is not installed the scanner degrades topass(never blocks on a missing optional dep).The input gate now also runs on the Shiny app’s real stream path (
server_chat.Rstream_task), not only the standalonecodeagent_stream_async(). Uploaded attachments and typed text in the app are scanned at edge 1 before reaching the model; a block ends the turn with a chat message, a redact continues with the sanitized input.UserPromptSubmithook can now block or add context (was notify-only). RenamedUserMessage->UserPromptSubmitto align with Claude Code’s public hook event name. A hook may returnaction = "block"(the prompt never reaches the model) or"add_context"(append text to what is sent) – never rewrites the user’s original wording, matching CC’s contract.run_user_message->run_user_prompt_submit.Hooks aligned with Claude Code’s 27 lifecycle events (was 12). Events with a real trigger fire live:
SessionEnd,PostCompact,StopFailure,Notification,TaskCreated/TaskCompleted(through the TaskCreate/TaskUpdate tools), andInstructionsLoaded(replayed from the loaded CLAUDE.md files).FileChanged/ConfigChangeare driven by thewatcherpackage and work in BOTH the Shiny app and the CLI REPL – the prompt reads keys non-blockingly so an idle session still dispatches filesystem-watch callbacks (needswatcherinstalled; no-op otherwise). Remaining CC events are defined for a complete allowlist but have no live trigger yet (Elicitation/ElicitationResultneed MCP elicitation;TeammateIdle/Setup/CwdChangedhave no matching phase;WorktreeCreate/WorktreeRemovedeferred until cross-process team events).AssistantMessagestays notify-only (Claude Code has no such event).Bounded value-match index:
register_data(max_index_values=)caps the value-match hash index (default 500000 values, ~65MB of keys). Benchmarked on open-source CDISC-ADaM-format example data from the {pharmaverse} project (inst/bench/value_match_benchmark.R): index memory grows linearly and unbounded (~130MB / 1M values), so on overflowregister_data()now errors and refuses the dataset (a partial index that silently drops tail values would fail open) – raisemax_index_values, register in smaller pieces, or set it toInf. Zero false positives on ordinary clinical prose and pharmaverse-format USUBJID/SUBJID caught, so the min_len/min_card thresholds are unchanged.Column-level raw access:
register_data(column_access=)grants per-column raw access on a protected data.frame (e.g. a publicTESTCDcodelist beside protected columns), reusing the assetnone/schema/scan/rawlevels split intoprompt/egress.prompt="raw"makesDescribeDataenumerate the real values (no k-anonymity suppression);egress="raw"drops the column from the value-match index. A raw edge requires a non-emptyreason; an override missing it now errors (registration is refused) rather than being silently dropped, so a mis-typed raw grant cannot pass unnoticed.coverage()$raw_access_columnscounts overrides.Extensible ingress blacklist: built-in
shield_ingress()rules moved to a grouped.DATA_SHIELD_INGRESS_RULESconstant and expanded (pandasto_*, more R writers,urllib/httpx/aiohttp,nc/scp/rsync//dev/tcp, inline-e/-ceval). Apatterns=name matching a built-in now replaces that rule (was append-only); new names are added. Hosts wanting file-managed blacklists read their own file into a named vector and pass it viapatterns=.Small-model semantic code reviewer: new
shield_reviewer()is an optional internal ingress rail (never a model-callable tool). It reviews only deterministic PII/value-sanitized tool code/arguments with a fresh, tool-less, history-free ellmer Chat. Explicitclient_factorywins; otherwise the parent provider is reused withCODEAGENT_FAST_MODEL(never silently the main model). Scope defaults to exec/write/net; risk/error independently choose ask/block; async turns await a timed promise. Structured JSON parsing and reviewer failures fail closed. Remote mode never receives raw data/output; raw review is reserved for an explicit future local-only egress mode.Data Shield egress approval:
shield_egress(on_fail="ask")pauses after a local tool executes but before its result reaches the LLM. Default choices are Redact/Block; dangerousALLOW RAW ONCEappears only with explicitallow_raw_approval=TRUEand never changes future policy. CLI uses synchronous selection; Shiny uses a promise-backed three-button interaction. Missing callbacks, invalid choices, errors and timeouts default to redact. Approval payloads/audit contain metadata only, never the raw result.Portable sandbox policy: new
shield_sandbox()keeps project/session-temp path capabilities atrwxwhile the central gate validates explicit path arguments against project/protected/temp roots, follows real paths to reject symlink escape, and enforces per-rootr/rw/rwx. Because portable path policy is not process isolation, every non-delegated exec tool fails closed until a full OS adapter is available; this includes path-declared tools that can run project configuration.backend="auto"honestly falls back to policy (or blocks in required mode); coverage/audit report the fallback. btw file tools are also covered, and btw RunR is not treated as an OS sandbox.Per-tool/agent Shield policy: new
shield_tool_policy()supports exact or*-glob rules withscan(default), explicit auditedbypass, anddenyindependently for execution, ingress and egress. Trusted tools such as KMPlot may bypass only egress while retaining ingress scans; deny blocks execution. Shield bypass never bypasses the separate central permission gate.Data Asset Policy separates what an asset is (
kind: dataset/spec/ document/synthetic) from what the LLM may see (llm_access: prompt/egress none/schema/scan/raw). Kind-specific defaults keep datasets schema-only while allowing specs/synthetic prompt content; egress never defaults raw. Raw access requires a reason, is session-scoped with optional expiry, is audited, and requires explicit provenance via$trusted_result(). Synthetic/raw always runs baseline PII/secret regex; spec/raw may explicitly disable it.Non-sensitive Data Shield audit log: every blocked/asked ingress decision and redacted/blocked egress event records timestamp, edge, tool name/call id, strategy, action, reason label, match count and score in the owning R6 instance. Raw tool arguments/results, matched values, span text, rows and hashes are never stored. Use
$audit(limit=),$clear_audit(), and$coverage()$audit_events;audit_maxbounds memory with oldest-first eviction.Universal Data Shield ingress scanning: new
shield_ingress()scans the arguments of every native/btw/MCP/host tool inside the single central permission gate before execution. High-confidence R/Python/Bash serialization, encoded output, network exfiltration, shell data-file display, registered-data preview calls, and custom PCRE rules mayblockor force the existingask_fnapproval UI. Read-only/unknown tools no longer bypass a Shield-forced ask; without a Shield, permission behaviour is unchanged.Composable egress scanners:
DataShieldnow executes an ordered scanner pipeline (list order = execution order) and supports runtime$add_scanner(name, fn). Newshield_regex()redacts or blocks unregistered email, phone-like values, common API-token prefixes, 18-character identity numbers, and custom named PCRE patterns. Scanner contracts are validated and fail closed; no custom S7 or Python dependency is introduced.Data Shield recursively covers foreground sub-agents: synchronous and concurrent
Agentsub-chats inherit the parentDataShieldbefore their first model request, and uninstrumented btw/custom-agent delegation is skipped.BackgroundAgentand/bgfail closed while a shield is active because a separate mirai process cannot safely inherit the session’s R6/index yet.Multi-user Shiny isolation:
codeagent_app(client_factory = function(session) ...)now creates a freshCodeagentClient/Chat inside every Shiny session; the no-client default uses this safe path automatically. Passing a pre-builtclientremains the explicitly single-user compatibility path. Data Shield protected values live in a session/thread-ownedDataShieldR6 that may be shared deliberately without any package-global index.Data Shield P0/P0.5/P1 (opt-in, default OFF) is now a single stateful
DataShieldR6 engine. The easydata_shield=list(shield_*())form creates a private R6; pass an explicitDataShield$new()to register uploads or share a policy across selected chats.shield$install(chat)wraps tool results;shield$register_data()builds high-entropy value indexes;shield$describe()powers the automatically installed strictDescribeDatatool. Bulk rows and targeted protected values are withheld; metadata exposes no distributions, counts, raw rows, or free-text examples. Scanner specs remain plain R functions/lists (no custom S7).Backend permission gate for host tools: new exported
install_permission_gate(chat, permission_mode, rules, tools, ask_fn, tool_meta)lets a harness-only client (register_tools = FALSE) put the central permission gate over host-attached tools (the gate is otherwise only installed by.register_all_tools()). The gate now also passes the tool-callidtoask_fns that accept it (ask_fn(name, input, id = NULL)), so hosts can match an approval prompt to theon_tool_requestpreview; legacy(name, input)ask_fns are unchanged.Backend embedding (Contract v1): documented, stable surface for hosting codeagent as a backend engine. New exported
register_tool_meta(name, capability)lets host apps declare a custom tool’s capability (read/write/exec/net) so the central permission gate governs it (an undeclared tool previously defaulted toreadand was allowed ungated). New exportedtool_result(value, kind, payload)builds a typed display card (text/table/image/code/diff/error) that reaches theon_tool_result$displaycallback and the Shiny app. Adds thebackend-integrationvignette + a reference example (inst/examples/backend_integration_demo.R) + a contract guard test.Concurrent sub-agents (opt-in via
settings$async_subagents, default OFF): theAgenttool can run asynchronously so multiple sub-agent delegations requested in a single turn execute concurrently (via ellmer’stool_mode = "concurrent") on the async streaming paths (CLI REPL / Shiny app). Sync one-shotcodeagent()transparently falls back to sequential sub-agents, since async (promise-returning) tools are invalid underChat$chat(). Also adopts ellmer 0.4.2 (set_model(),chat_posit()).Background sub-agents (opt-in via
settings$background_agents, default OFF; requiresmirai): a newBackgroundAgenttool delegates a task to a fire-and-forget sub-agent running in amiraidaemon and returns immediately (non-blocking). Its result is polled and surfaced back to the model on a later turn via the system reminder, mirroring Claude Code’s async agents. Users can also spawn and inspect background agents directly with the/bg <task>and/bgstatusslash commands (REPL and Shiny).
codeagent 0.1.0
First public release. codeagent is an R-native reimplementation of a command-line coding agent, built on ellmer and btw. It provides the agent harness (loop, tools, permissions, compaction, hooks, skills) plus a CLI REPL and a shiny user interface.
Model tier env var rename — breaking changes
Three environment variables have been renamed to remove vendor-specific names. Update your .Renviron / settings.json env block accordingly:
| Old | New | Meaning |
|---|---|---|
CODEAGENT_DEFAULT_SONNET_MODEL |
CODEAGENT_MODEL |
Everyday main model |
CODEAGENT_DEFAULT_OPUS_MODEL |
CODEAGENT_HEAVY_MODEL |
High-capability model |
CODEAGENT_SMALL_FAST_MODEL |
CODEAGENT_FAST_MODEL |
Cheap/fast model |
Tier aliases used in /model and codeagent.md also changed: "sonnet" → "main", "opus" → "heavy", "haiku" → "fast".
CODEAGENT_MODEL now serves dual purpose: it sets both the default model and the "main" tier alias (previously CODEAGENT_MODEL and CODEAGENT_DEFAULT_SONNET_MODEL were separate; they are now merged).
CLI/ink unified entry point — breaking changes (plan #20)
Default permission mode is now
"default"across all entry points (CLI, Shiny, ink). Previously all CLI subcommands defaulted to"bypass". Write operations (file edits, shell commands) now prompt for approval unless you explicitly opt into bypass mode.-
-y/--yolo— new global flag for the CLI that enables bypass mode (skips all permission prompts). Equivalent to Claude Code’s--dangerously-skip-permissions. Short-hand:-y.codeagent -y # bypass REPL codeagent app -y # bypass Shiny app codeagent run "q" -y # bypass one-shot query codeagentwithout a subcommand now starts the interactive REPL directly (equivalent tocodeagent chat). Previously a subcommand was required.-p/--print-mode— new flag for one-shot non-interactive output.codeagent "query"orcodeagent -p "query"runs a single query and exits.-mnow means--model(breaking). The old-m/--modealias has been removed. Use-y/--yolofor bypass mode instead.-
ink_ui()gainsyolo = FALSEparameter. WhenTRUE, setsINK_YOLO=1so the codeagent backend runs in bypass mode. Theinkaiterminal command also accepts-y/--yolo.ink_ui("codeagent", yolo = TRUE) # bypass # or from terminal: inkai codeagent -y # bypass INK_YOLOenv var — set to"1"to enable bypass mode in ink when launching theinkaicommand directly:INK_YOLO=1 inkai codeagent.codeagent_app(permission_mode = "default")unchanged — Shiny was already correct; passpermission_mode = "bypass"explicitly when needed.R/cli_dispatch.R— new internal helpers.ca_resolve_mode()and.ca_dispatch()expose CLI dispatch logic as testable pure functions.
Unified agent streaming API (plan #19)
codeagent_stream_async()— new exported function. Streams one agent turn asynchronously (coro::asyncpromise). Runs the full turn pipeline (compaction, system-reminder injection, session save, cost tracking viaget_cost(include="last")). Fires typed callbacks:on_delta,on_thinking,on_tool_request(pre-gate, from stream chunk),on_tool_result(with typeddisplaycontract from.adapt_tool_result()),on_error,on_usage. Supportsstream_controllerfor cancellation andtool_modefor concurrent tool execution.codeagent_stream()— synchronous wrapper aroundcodeagent_stream_async()usinglater::run_now()to pump the event loop. Handles Ctrl+C gracefully (cancels the stream viastream_controller, does not re-throw the interrupt condition). Intended for CLI and ink frontends.Turn pipeline helpers (
R/turn_pipeline.R, internal):.turn_setup()consolidates compaction + resource replacement + system-reminder injection into one call..turn_teardown()consolidates session save + usage +cost_last. Both are now shared by console, Shiny, and ink.Shiny system-reminder injection fixed.
server_chat.R’sstream_tasknow injects the<system-reminder>block (date/iteration/cwd/memory) on every turn, matching the behaviour of the console REPL andagent_loop().Console Ctrl+C repair.
codeagent_console()now creates astream_controllerper turn and catchesinterruptconditions, cancelling the stream gracefully. Previously Ctrl+C could corrupt the chat state.Callback deduplication.
.register_repl_tool_callbacks()is now guarded by.chat_once()to prevent stacking display callbacks ifcodeagent_console()is called more than once on the same chat object..patch_interrupted_chat()retired. Removed from all call sites. ellmer 0.4.0+ (#840) and 0.4.1+ (#643) handle orphaned tool requests andAssistantPartialTurnautomatically.inkAssistantUItool cards upgraded.ink_reply_stream()now callscodeagent_stream()when available (full turn pipeline + display contract).on_tool_resultreceives a richdisplayfield (title/kind/payload) instead of a plain string. Theink_server()initialises per-sessionCompactionController/ContentReplacementState/session_idso turns are properly managed.
Multi-agent teams (post-0.1.0 additions)
-
Task DAG.
team_coordinate()gainsblocked_by— task dependencies given by 1-based index. A task is only claimed once all its blockers aredone, so workers respect ordering while still parallelising independent tasks. Cyclic graphs are rejected up front. The shared board’s claim is now dependency-aware and atomic (BEGIN IMMEDIATE). -
Worktree isolation + crash recovery.
worktree = TRUEruns each worker in its own git worktree;board_reclaim_stale()(wired into the worker loop) resets a crashed worker’s timed-out task back to pending so its dependents are never blocked forever. -
Event-driven board.
board_watch()(built on thewatcherpackage) reacts to board changes without polling, powering an event-driven coordinator / live board view (falls back to polling when watcher is unavailable). -
LLM-lead coordinator.
team_lead(goal, max_rounds =)faithfully ports Claude Code’s COORDINATOR_MODE: a lead model decomposes the goal into a task DAG, the work-stealing team runs it, then the lead reviews results and either finishes or adds a follow-up round (bounded loop; decompose/review/coordinate steps are injectable for testing). -
Live dashboard.
team_dashboard(db_path)is a standalone Shiny app that monitors a running team’s board in real time — task table (coloured by status), a progress bar, and the inter-agent message log.
Shiny app UX (post-0.1.0 additions)
Instant startup. The UI shell now renders immediately; the slower tool + skill registration runs in the background behind a prominent “Initializing codeagent…” overlay, with the chat input gated until it completes. Pass a bare
ellmer::Chattocodeagent_app()for this lazy path.Skill metadata disk cache.
list_skills_meta()now caches parsed skill metadata on disk (<config>/cache/skills/, keyed by cwd + aSKILL.mdmtime/count signature), so the slash typeahead and skill tool are near-instant on every launch after the first (a disk hit skips the ~20s directory scan). The cache self-invalidates when anySKILL.mdchanges or a skill is added/removed.Single-file viewer. Clicking a file in the Files tree now opens it in one static, scrollable “File” tab (code / Markdown / image / CSV) with a filename header and close button, replacing the old per-file tabs that could overflow and cover the tab strip.
Security & testing improvements (post-0.1.0 additions)
keyring integration (
R/keyring.R): Optional API key storage via the OS credential store (keyringpackage).setup.Roffers the keyring as an alternative to~/.Renvironwhen the backend is available. Includes.keyring_available()(session-cached probe),.keyring_store_key()with graceful fallback to~/.Renviron, and.keyring_get_key(). On headless/server environments the keyring backend probe returnsFALSEand all functions degrade silently to the existing~/.Renvironpath.webfakes agent integration tests (
tests/testthat/test-webfakes-agent.R): 12 tests that mock the LLM API endpoint withwebfakes, exercising the full agent loop — tool dispatch (Read, Write, Bash), permission gate (bypass vs plan), error recovery (HTTP 500), and skill invocation — without hitting a real LLM.Explicit tool names:
bash_tool(),read_tool(),write_tool(),edit_tool(),multi_edit_tool(),glob_tool(),grep_tool(),ls_tool()now passname=toellmer::tool()so the model can refer to tools by their canonical names (Bash, Read, Write, Edit, MultiEdit, Glob, Grep, LS).
Agent harness
- Agentic loop (
agent_loop()) with max-turns, token budget, verification, and error recovery (prompt-too-long, rate-limit, network, and auth handling). - Seven-mode permission system:
default,plan,accept_edits,bypass,dont_ask,auto, andbubble(sub-agent decisions bubble to the parent). Fine-grained rules match on tool arguments (e.g.Bash(npm run test *)). - Twelve-event hook system covering tool, permission, message, and lifecycle events, configurable declaratively from
settings.json. - Five-level context compaction (snip, session memory, full summary, prompt fallback, context collapse).
- System prompt with tone, task, convention, tool-use, and R-specific guidance.
Tools
- Core tools:
Bash,Read,Write,Edit,MultiEdit,Glob,Grep,LS. -
RunRexecutes R code behind the permission gate; with sandboxing enabled it runs in an isolatedcallrsubprocess with a scrubbed environment (secrets hidden), no.Renvironreload, and a wall-clock timeout. -
btwtool groups (docs, git, pkg, env, etc.), web fetch and search, notebook tools, task and persistent-todo tools. - Optional codebase retrieval via
ragnar(vector + keyword search).
Coordination
- Sub-agents via
agent_tool(), with optional git-worktree isolation and persistent “sidechain” sessions. - Parallel teams:
team_run()(fixed fan-out) andteam_coordinate()(work-stealing over a shared SQLite board with inter-agent messaging), both capped to the container’s CPU quota viaparallelly. - Plan-mode tools let the model enter and exit read-only planning mid-turn.
State and configuration
- Sessions saved as JSONL with lossless tool-call preservation; fork, rename, tag, resume, and rewind (
truncate_chat_turns()//rewind). - Auto-memory persisted across sessions, with relevance selection by a small fast model.
-
settings.jsonconfiguration mirroring command-line agents:envblock, model tiers, permissions, hooks, MCP servers, sandbox, effort level, and more. - Model switching mid-conversation (
switch_model()), lossless where possible.
Interfaces
- CLI:
codeagentexecutable withrun,chat/repl,app,skills,mcp, andinfosub-commands; the REPL streams output, shows tool activity, and renders reasoning blocks. -
shinyapp (codeagent_app()) with tool cards, session management, and theme options. - MCP server (
codeagent_mcp_server(), stdio and HTTP) and MCP client (register_mcp_client(), stdio) for external tool interoperability.