Skip to content

Releases: BlakeHung/acp-bridge

v0.9.2

Choose a tag to compare

@github-actions github-actions released this 06 Oct 03:53
abadae1

Added

  • Thinking-mode tool-call recovery — reasoning models (DeepSeek-R1,
    Qwen 2.5/3, GLM) frequently emit tool-call JSON inside the content
    channel instead of the structured tool_calls field. acp-bridge
    previously treated such responses as a final text answer, meaning the
    agent never dispatched the requested tool and the session stalled.
    The engine now sanitizes the reasoning scaffolding
    (strip_thinking_blocks) and recovers embedded tool-call JSON —
    fenced ```json / ```tool_call blocks, bare balanced objects with
    name + arguments/args/parameters/input, or the whole-call
    {"function": {…}} shape — reproducing the OpenAI-style tool_call
    objects the structured path would have produced. Recovered rounds
    continue the normal agentic loop and are logged for debugging.
    Eleven new unit tests (thinking_recovery_tests). Project page:
    wchung.tw/acp-bridge/.

v0.9.1

Choose a tag to compare

@WCHungBlake WCHungBlake released this 03 Oct 19:24

Fixed — ACP v2 wire-shape blockers

The 0.9.0 release shipped with three wire-shape violations against the
v2 protocol that a spec-compliant v2 Client (e.g. an early OpenCode v2
preview) would have rejected. All three are addressed in this release:

  • state_update discriminator — the v2 schema requires every
    state_update payload to have a state field set to one of
    "running" | "idle" | "requires_action". The previous code emitted
    "available": false (a non-spec field) and was missing state. Now
    emits {sessionUpdate: "state_update", state: "idle", stopReason?}
    per the IdleStateUpdate schema.
  • session/prompt v2 response carries messageId — the v2
    PromptResponse is {required: ["messageId"]}. The previous v1-style
    response ({stopReason, status, text}) would have failed schema
    validation. acp-bridge now mints a UUID-derived messageId per prompt
    and emits the v2 wire shape for v2 Clients. The v1 wire is unchanged.
    Legacy status / text / stopReason now live on state_update
    for v2 Clients, exactly as the v2 spec requires.
  • v2 session lifecycle methods — the v2 baseline includes
    session/new | session/list | session/resume | session/close | session/prompt | session/cancel | session/update. acp-bridge
    previously only implemented the v1 surface (session/new,
    session/end, session/prompt, session/cancel). New:
    • session/close (v2 baseline; shares the implementation with
      session/end)
    • session/list (v2 baseline; returns active sessions as
      {sessions: [{sessionId, cwd}], nextCursor: null})
      session/delete (v2 optional) and session/resume /
      session/load return graceful -32601 not_implemented /
      -32001 no_persistence rejections with the stable data.reason
      field.

Fixed — Ollama native protocol bugs

Three pre-existing bugs in the Ollama native code path that had been
documented as fix-plan priorities P0-C. None were wired through the
test suite (tests pointed the harness at 127.0.0.1:1, so Ollama
native was never exercised in CI):

  • tool.arguments object vs string — Ollama native /api/chat
    returns function.arguments as a JSON object; OpenAI-compatible
    backends return it as a JSON-encoded string. The previous code called
    as_str() on the value and silently fell back to "{}" when it
    wasn't a string, which meant every Ollama native tool call ran
    with empty arguments. Now handles object / array / string uniformly.
  • options.* sampling fields — Ollama native wants
    temperature and max_tokens (renamed num_predict) inside an
    options object; OpenAI-compatible expects them at the top level.
    The previous code only sent top-level fields, so every Ollama
    native request silently used the model's defaults for sampling.
  • format_tool_result Ollama field — Ollama native tool messages
    use {"role": "tool", "content": …} and ignore (some versions
    reject) the tool_call_id field that acp-bridge included. The new
    helper emits tool_call_id only when the backend is not Ollama
    native.

Fixed — sandbox escapes

  • search_code walks symlinks — previous code used path.is_dir()
    / path.is_file(), which follow symlinks. A symlink inside the
    sandbox pointing at /etc/passwd (or anywhere outside working_dir)
    would be read and its contents returned. Now:
    • Skip entries whose symlink_metadata reports file_type().is_symlink()
    • Canonicalize the entry and skip it if it does not start with the
      canonicalized working dir
    • Bound recursion depth to MAX_LIST_DEPTH * 4 to prevent
      adversarial directory structures
  • web_fetch redirects bypass LLM_WEB_ALLOWLIST — reqwest's
    default redirect policy follows up to 10 hops to any host. A
    server on an allowlisted domain could 302 to an internal host and
    acp-bridge would happily return the body. Now uses
    redirect::Policy::custom that re-validates the next hop's host
    against LLM_WEB_ALLOWLIST on every redirect, with a 5-hop cap.

Changed

  • docs/scope.md synced with current state — the previous version
    still said "Not a v2 protocol agent yet" (incorrect as of 0.9.0),
    listed the wrong tool set (5 tools instead of the 11 actually
    shipped in 0.8.2), and referenced a codex_style.rs test file
    that was renamed to minimal_style.rs long ago. Now reflects the
    real v1 / v2 dual implementation, the full tool surface, and the
    current tests/clients/ layout.
  • CHANGELOG.md corrections — 0.9.0 entry over-claimed
    Session::protocol_version is read at emit sites; the field is
    stored on each Session but the emit helpers currently use
    AppState.protocol_version (one Client per process in practice).
    0.8.2 entry under-reported the test count.

Tests

171 tests total (from 168 in 0.9.0). Added in tests/clients/protocol_version.rs:

  • v2_session_prompt_response_carries_message_id — asserts the v2
    PromptResponse carries the required messageId and does not
    carry the legacy v1 stopReason / status fields.
  • v2_session_close_succeeds_and_v2_session_delete_gracefully_rejects
    — confirms the new session/close routing and the stable
    data.reason: "not_implemented" error on session/delete.
  • v2_session_list_returns_session_info_with_cwd — asserts
    session/list returns the active sessions in the v2 wire shape.

Hardened existing tests:

  • v2_emits_state_update_at_end_of_turn now asserts the
    state: "idle" discriminator (the previous version only checked
    the discriminator string, which let the bug through).
  • tests/clients/inspector_style.rs::inspector_style_session_*_returns_method_not_found_gracefully
    were updated to positive tests — the methods are now implemented
    and the previous negative assertions no longer held.

v0.9.0

Choose a tag to compare

@WCHungBlake WCHungBlake released this 03 Oct 19:24

Negotiate protocolVersion at init time and emit v1 or v2
session/update payloads accordingly. Same code base, dispatch by
version. The implementation adds:

  • protocol::ProtocolVersion enum (V1, V2, LATEST) and the
    Session::protocol_version field for defense-in-depth and future
    per-session routing.
  • AppState::protocol_version (set by run_acp_loop from the negotiated
    value) so every emit helper branches on the same source of truth.
    AppState: Clone is hand-written; sessions are wrapped in
    Arc<RwLock<...>> so the clone shares the map.
  • acp::notify_*_for(version, …) dispatchers that route to the v1 or
    v2 implementation. The plain (no-suffix) notify_* functions stay
    as the v1 shortcut for code paths that have not migrated.
  • acp::notify_state_idle_for() for v2-only state_update (IdleState).
  • negotiate_protocol_version(params) in main.rs — implements the
    ACP spec's "pick the highest version we both support" rule.
    Clients that omit protocolVersion fall back to v1 (conservative
    default). Clients requesting a version > 2 fall back to v2 with a
    warning.
  • v2-shaped InitializeResponse: unified info + capabilities
    (role-agnostic), with capabilities.session.prompt.image expressed
    as {} (capability marker) when supported.
  • v2-shaped session/update discriminators:
    • agent_message_chunk, agent_thought_chunk (chunk variants)
    • tool_call_update (instead of v1's tool_call)
    • plan_update with plan: { type: "items", planId, entries[] }
  • v2 baseline session/close (alias for v1's session/end) and
    session/list (returns active sessions as
    {sessions: [{sessionId, cwd}], nextCursor: null}).
  • v2 graceful rejects for session/delete, session/resume,
    session/load: -32601 not_implemented / -32001
    no_persistence with stable data.reason.
  • LlmConfig.build_body is now backend-aware: OpenAI-compatible gets
    top-level temperature / max_tokens; Ollama native gets them
    inside options (temperature + num_predict). The previous
    top-level-only shape meant every Ollama native request silently used
    the model's defaults for sampling.

Tests

9 new tests in tests/clients/protocol_version.rs:

  • v2_initialize_returns_unified_capabilities_shape
  • v2_tool_call_uses_tool_call_update_not_tool_call
  • v2_plan_uses_plan_update_with_plan_id
  • v2_emits_state_update_at_end_of_turn
  • v2_does_not_emit_state_update_for_v1_clients
  • v1_client_gets_legacy_shapes_unchanged
  • inspector_style_session_list_returns_empty_array_with_no_sessions
    (updated from the pre-0.9.0 negative-test now that session/list
    is implemented)
  • inspector_style_session_close_succeeds_and_variants (updated
    similarly)

168 tests passing; cargo fmt --check and
cargo clippy --all-targets -- -D warnings clean.

Migration

  • v1 Clients see no behavior change. The plain notify_* functions
    still emit v1, and every emit site in main.rs was migrated to
    use the _for(version, …) helpers that branch on the negotiated
    version.
  • Existing inspector_style_session_list_returns_method_not_found_gracefully
    and inspector_style_session_close_returns_method_not_found_gracefully
    tests were renamed and rewritten as positive tests. The original
    intent of those tests (probe capability, get clear error) is
    preserved — session/close now returns -32001 UnknownSession
    instead of -32601 MethodNotFound when called on a non-existent
    session, and session/list returns {sessions: []} with no
    sessions open.

Co-Authored-By: Claude noreply@anthropic.com

Full Changelog: v0.8.2...v0.9.0

v0.8.2

Choose a tag to compare

@WCHungBlake WCHungBlake released this 03 Oct 19:24

Added

  • New tools — four more built-in tools round out the surface so AI
    agents can stay inside acp-bridge instead of falling back to
    their own knowledge:
    • edit — surgical string replacement. Replace exactly one
      occurrence of old_text with new_text in an existing file.
      Refuses to act if old_text is missing or appears more than once,
      so the model has to re-read the file rather than guess.
    • write_file — create or overwrite a file with new content.
      Sibling to edit for whole-file rewrites; rejects .. escapes.
    • web_fetch — fetch a URL over HTTP/HTTPS and return the body
      as text. HTML is reduced to readable text (scripts/styles
      stripped, tags removed, whitespace collapsed). 5 MB body cap,
      30 s timeout.
      Off by default: requires LLM_WEB_ALLOWLIST to be set to a
      comma-separated list of host suffixes (e.g.
      LLM_WEB_ALLOWLIST=docs.rs,crates.io). Empty allowlist blocks
      every request. This is an opt-in safety boundary so a
      misconfigured sandbox cannot exfiltrate to internal
      infrastructure.
    • git_status, git_diff, git_log, git_commit —
      read-only and write git operations, all run inside the session
      working directory. git_diff accepts an optional path and a
      staged: true flag (--cached). git_log accepts max_count
      (clamped to 1–200, default 20) and an optional path filter.
      git_commit stages the listed paths (or git add -u when
      paths is omitted) and commits with the supplied message.
  • acp-bridge now publishes four plan notification helpers —
    acp::PlanEntry, acp::notify_plan, acp::notify_session_info,
    acp::notify_usage, acp::AvailableCommand,
    acp::notify_available_commands. Engine hooks fire
    available_commands_update and session_info_update immediately
    after session/new, and usage_update + session_info_update
    after session/prompt returns.
  • LlmConfig.context_size — model context window in tokens,
    surfaced as usage_update.size. Override via LLM_MODEL_CONTEXT
    env var or [llm].model_context config field (default 32768).
  • PromptResult::usage carries an estimated used token count
    (chars / 4 across the session history) so the usage_update has
    a number to ship. Local backends rarely stream stable per-turn
    token counts; this is intentionally approximate.
  • acp::kind_for_tool now classifies edit, write_file,
    web_fetch, and the four git_* tools so Clients render the
    right icon and affordance.
  • Classified backend errors — LlmErrorKind (Unreachable,
    RateLimited, ServerBusy, Auth, BadRequest, NotFound,
    Timeout, ParseError, Unknown) and LlmError::is_retryable().
    chat and stream_chat now return Result<_, LlmError> instead
    of plain strings. Failed turns emit a structured
    error.data.category + error.data.retryable in the JSON-RPC
    response so Clients can branch on it (e.g. "auto-retry on
    backend_unreachable, show 'check your model name' on
    not_found").

Changed

  • Wire order on session/prompt and session/new — the
    post-event notifications (available_commands_update,
    session_info_update, usage_update) are emitted before the
    JSON-RPC response, not after. Clients that buffer the entire
    notification stream per turn (most ACP Clients) see the
    notifications bound to the right sessionId; Clients that read
    strictly one-line-at-a-time still get the response on the line
    after the notifications.
  • PromptResult gains error_class: Option<LlmErrorKind> and
    error_retryable: bool
    so the engine's failure classification
    reaches the JSON-RPC response without string-matching.

Tests

  • 14 new unit tests (src/tools.rs): write / edit (unique match,
    missing, ambiguous, empty), web_fetch allowlist enforcement, HTML
    reduction, git status.
  • 3 new unit tests (src/llm.rs): LlmErrorKind::as_str stability,
    retryable classification, status-code → kind mapping.
  • All existing test suites still pass; 159 tests total.

v0.7.8

Choose a tag to compare

@WCHungBlake WCHungBlake released this 27 Jul 15:05

Breaking Changes

  • A2A mode removed — --a2a HTTP server with Agent Card support is no longer available
  • Client mode removed — --client external ACP agent spawning is no longer available
  • Configuration changes — [a2a] and [agent] sections in config.toml are no longer supported

Added

  • Backend abstraction layer — New Backend enum (Ollama/OpenAi) encapsulates protocol-specific logic for message formatting, response extraction, and tool-call handling
  • Simplified architecture — Focused ACP-only adapter with cleaner codebase
  • Benchmark mode — Added --bench flag for performance testing

Removed

  • A2A implementation — src/a2a.rs (299 lines) deleted
  • Client implementation — src/client.rs (869 lines) deleted
  • Client tests — tests/client_test.rs (173 lines) deleted
  • Marketing materials — DEMO-AND-MARKETING.md (718 lines) and marketing-drafts.md (176 lines) deleted
  • Dependencies — axum and libc crates removed from runtime dependencies (axum kept as dev-dependency for tests)

Changed

  • README updates — Removed --a2a HTTP server mention, added --bench example, updated Project status to reflect ACP-only scope
  • Configuration system — Simplified to only support LLM configuration
  • Help text — Updated to reflect ACP-only positioning (removed --a2a and --client options)
  • Project status — Updated to reflect v0.7.8 ACP-only scope

Internal

  • Backend-specific logic moved — Protocol quirks moved from engine.rs to llm.rs Backend enum
  • Code reduction — 1,649 lines of code removed overall
  • Simplified RunMode enum — Now only Acp and Bench modes

Migration Notes

Users relying on A2A mode should migrate to ACP mode with their ACP harness. Users using client mode should configure their harness to spawn acp-bridge directly via stdin/stdout JSON-RPC.

v0.7.7

Choose a tag to compare

@github-actions github-actions released this 02 Jun 07:06

Fixed

  • session/prompt final response was missing the accumulated text — handle_acp_prompt previously sent the assistant's final text exclusively through Notification::TextChunk and replied with {"status": "completed"}. Upstream pipelines that consume the final response (or that treat ToolDone("llm_chat","completed") as the turn boundary and stop reading further notifications) saw an empty reply even though the chunks had been streamed. The final response now also carries text: result.text so non-streaming consumers and edge-case race conditions still get the body. Reviewer-flagged by Eren.

v0.7.6

Choose a tag to compare

@github-actions github-actions released this 02 Jun 03:52

Fixed

  • <sender_context> metadata in user prompts made models emit empty replies with no tool calls — OpenAB-style harnesses prepend a <sender_context>{…json…}</sender_context> block to the user message. Several local LLMs (observed on Qwen3-Coder via Ollama) interpret the XML wrapper as a directive and stall — the model returns empty content with no tool calls, which surfaces upstream as "the agent doesn't reply" and "the agent doesn't know about brain/KB". engine::strip_sender_context now detects the block, removes it from the forwarded user text, and logs the captured inner string at debug level for traceability. Both the ACP (handle_acp_prompt) and A2A (handle_message_send) entry points strip before the empty-prompt guard and before passing to session_prompt. Four unit tests cover the leading-block case, the no-block passthrough, an unterminated open tag, and the all-metadata edge case. Reviewer-flagged by openab-rukawa.

v0.7.5

Choose a tag to compare

@github-actions github-actions released this 01 Jun 18:06

Fixed

  • Inbound image MIME type was discarded and rewritten as JPEG — both engine::extract_image_parts and the per-block path inside session_prompt had only kept the base64 data and hard-coded data:image/jpeg;base64,… when forwarding to OpenAI-compatible backends. Any ACP/A2A client sending PNG/WebP/GIF content was therefore mislabeled, which can break vision-model decoding or yield undefined multi-modal behaviour. Image extraction now returns a new ImageBlock { data, mime_type }, the per-block mimeType is threaded through, and session_prompt emits data:<mime>;base64,<data> using the client's declared MIME (with image/jpeg only as a fallback for clients that omit the field). Reviewer-flagged by Eren.
  • A2A transport silently dropped image inputs — handle_message_send only extracted text from message.parts and always called engine::session_prompt(..., &[], None), so text+image A2A requests lost their images and image-only A2A requests were rejected as empty. initialize() already advertises agentCapabilities.promptCapabilities.image: true, so the A2A path now honors it: both text and image parts are extracted, an empty prompt is rejected only when both are absent, and images are forwarded to session_prompt. Reviewer-flagged by Eren.

v0.7.4

Choose a tag to compare

@github-actions github-actions released this 01 Jun 17:19

Fixed

  • bench.rs TOTAL aggregate was dragged down by error fixtures — when a fixture timed out or failed, its wall_ms was still summed into the aggregate even though its completion-token count was absent. The aggregate tok/s now skips error rows. Reviewer-flagged by Mikasa.
  • OpenAI-mode tok/s was not labelled as wall-clock-derived — Ollama-native mode computes tok/s from eval_duration (decode only), while OpenAI-compat mode divides by wall_ms (which includes TTFT and transit). The column header now shows tok/s* in OpenAI mode and a footnote explains the difference, so readers don't conclude OpenAI backends are slower than they actually are. Reviewer-flagged by Mikasa.

Changed

  • bench::Fixture gains an Option<&'static str> system_prompt field — decode-heavy fixtures (explain_concept, summarize) now run without a "concise" system prompt so they produce enough tokens to make the timing meaningful. Other fixtures keep a tight prompt because they're intentionally short. Reviewer-flagged by Mikasa.

Internal

  • engine.rs user-message path — removed a dead inner if user_images.is_empty() inside the OpenAI-compat else arm. The outer branch already guarantees the slice is non-empty there; the inner check could never fire. Reviewer-flagged by Eren.

v0.7.3

Choose a tag to compare

@github-actions github-actions released this 01 Jun 14:43

Important

  • Skip 0.7.2 on crates.io — it is the buggy pre-fix code from a cancelled release run. The 0.7.2 git tag and Docker image at ghcr.io/blakehung/acp-bridge:0.7.2 point at the fixed code (commit fc0b9c7), but crates.io permanently locked the version at the earlier b253172 snapshot before the cancel landed. Use 0.7.3+ from crates.io. The 0.7.2 entry below still describes the intended contents; 0.7.3 ships those plus the second-round reviewer findings.

Fixed

  • NVIDIA product names containing commas were truncated — parse_nvidia_smi now splits with rsplitn(2, ',') (from the right) instead of splitn(2, ','), so names like "NVIDIA GeForce RTX 4090, Ada" parse correctly and the VRAM column lines up. Reviewer-flagged by Mikasa.
  • parse_rocm_smi rejected MB-unit VRAM — older rocm-smi versions emit VRAM in bytes, newer ones emit MB. The old > 100_000_000 filter discarded MB values entirely. Now takes the max parseable number on the row and converts only when it looks like bytes. Reviewer-flagged by Mikasa.
  • AMD Vulkan-only fallback missed cards with unprefixed / upper-case vendor IDs — scan_sysfs_amd now normalizes the sysfs vendor string and accepts both 0x1002 and 1002. Reviewer-flagged by Mikasa.
  • First fixture in --bench ate the cold-start cost — bench::run now does a discarded warm-up chat() before the first measured fixture to prime model load + cache. Reviewer-flagged by Mikasa and Armin.

Changed

  • session/load, session/resume, session/set_mode now return -32601 — these are not supported (we don't advertise loadSession, sessions are created without modes), so ACP capability-based negotiation calls for method-not-found rather than the -32001/-32602 codes the previous patch used. Message strings still explain the underlying reason. Reviewer-flagged by Armin.