Skip to content

cmagent v0.1.0

Choose a tag to compare

@coremail-cyt coremail-cyt released this 09 May 23:33
· 34 commits to main since this release

Changelog

Notable changes by release. Date format YYYY-MM-DD.

v0.1.0 -- 2026-05-09

Initial public release.

Platforms: Linux x86_64 / aarch64, macOS x86_64 / aarch64, Windows x86_64.

Highlights

  • Multi-provider agent loop (Anthropic, OpenAI-compatible, GLM) with SSE streaming
  • 20+ built-in tools: file operations, shell, web fetch/search, CDP browser automation,
    persistent memory (brain), sub-agent delegation, unified messaging
  • Channel integrations: Telegram, Lunkr (p2p + group), Slack, Discord, WeChat
  • SKILL.md-based prompt extensions with keyword/tag activation and slash command support
  • 4-layer security model: input guard, application policy, OS sandbox, output safety
  • Gateway (HTTP + WebSocket, multi-user RBAC), ACP stdio server, Ralph Loop
  • Config wizard, cmagent doctor, session undo/redo/retry

Development log (pre-release)

[Unreleased] -- 2026-05-01

Preserve reasoning_content across turns (DeepSeek V4 thinking mode)

DeepSeek V4 returns a reasoning_content field on assistant turns
(thinking mode) and rejects follow-up requests that don't echo the
prior turn's reasoning chain back:

API error 400: The reasoning_content in the thinking mode must
be passed back to the API.

The OpenAI provider was stripping reasoning entirely when
serializing message history. Three changes round-trip it:

  1. ChatMessage gained reasoning_content: Option<String>
    (skip-serializing-if-none for back-compat with old session
    JSON and providers that don't accept the field).

  2. New constructor ChatMessage::assistant_with_tools_and_thinking;
    the agent's turn-loop now uses it for both tool-call turns and
    final-answer turns so every prior assistant message carries its
    reasoning.

  3. The OAI request body serializer
    (OpenAiProvider::build_request_body) only includes
    reasoning_content on outgoing messages when the model is in
    the round-trip allowlist (requires_reasoning_round_trip,
    currently deepseek-v4-*). Other thinking-mode models on the
    OpenAI-compatible wire (DeepSeek R1's docs say to omit
    reasoning_content; GLM / Qwen behavior unverified) are
    unaffected -- they still get the prior pre-fix behavior of no
    reasoning in messages. Storage in ChatMessage happens
    regardless so the data survives a model switch within a
    session.

    Additional case: legacy session histories saved before
    ChatMessage gained the field have None on every assistant
    turn. For V4 the field MUST be present (DeepSeek rejects
    follow-ups whose prior assistant turn lacks it), so the new
    build_reasoning_content_for_message helper emits an empty
    string in that case -- the field is there, just empty, which
    V4 accepts. Without this fallback, switching an existing
    session to V4 would 400 on the first turn.

Provider catalog: deepseek-v4-pro / deepseek-v4-flash flipped
to supports_thinking = true to reflect actual V4 behavior.

Tests:

  • crates/cmagent-provider/src/types.rs::tests (4):
    assistant_message_omits_reasoning_when_absent,
    assistant_with_thinking_preserves_reasoning,
    assistant_with_empty_thinking_drops_field,
    chat_message_deserialize_back_compat.
  • crates/cmagent-provider/src/openai.rs::tests (1):
    round_trip_only_for_deepseek_v4 locks in the allowlist
    so other models (R1, V3, GPT, GLM, Qwen) stay opt-out.

/model picker: read providers from disk on every open

Editing a provider via the in-TUI /config wizard wrote the new
config to ~/.cmagent/providers/*.toml but the subsequent
/model picker still showed the snapshot loaded at TUI startup.
Users had to exit and re-launch to pick the new model. Both ends
fixed:

  • tui2/key_handler.rs::UserCommand::Model now does
    ConfigLoader::from_default().load_providers() on each open
    (matching the /agent picker's existing fresh-load pattern)
    instead of using the cached opts.providers snapshot.
  • agent/commands.rs::handle_command(UserCommand::Model) reloads
    providers_config from disk before resolving the new
    provider_id:model_id. Without this, edits to base_url /
    api_key_env made via /config would be ignored on switch
    because the agent's snapshot was taken at build time.

DeepSeek provider catalog

Add the V4 lineup (deepseek-v4-pro, deepseek-v4-flash) to
assets/provider_catalog.toml per the model ids returned by the
DeepSeek API. deepseek-v4-pro is the new default; flash ships
as a cheap-tier alternative. Old deepseek-chat (V3 alias) and
deepseek-reasoner (R1) entries kept for backward compatibility.
Pricing is a placeholder until DeepSeek publishes official numbers.

Doctor: provider key diagnostics

cmagent doctor now prints the masked api key + length next to
each provider's "set" line so users can sanity-check the key
cmagent loaded against what their dashboard shows. Catches stale
.env files and copy-paste truncations that the bare "is set"
line previously hid. Trailing whitespace also gets a dedicated
warning since load_env_file already trims it but the dashboard
might not.

Lunkr p2p: keyboard response classifier bound to fid=1212

The Lunkr server moved inline keyboard responses from fid=1211
(multiplexed with the chat-window-opened P2pPing) to fid=1212
(dedicated channel) in the 2026-04-27 protocol revision. The
classifier in cmagent_channels::lunkr::p2p previously matched
attachments[0].t == 22 on any fid -- behavior happened to keep
working under both protocols, but the loose check would also
classify any future stray t=22 payload on an unrelated fid as
a keyboard response.

p2p::classify now requires the canonical wire shape
(fid==1212 && attachments[0].t==22) for the KeyboardResponse
arm. Legacy fid=1211 + t=22 payloads fall through to
OtherSignal; bare t=22 with no fid falls through to
NotSignal. The fid==1212 branch documents the exact line to
widen if a mixed-server environment ever needs to bridge to
older Lunkr backends. [9ba5b9f]

Tests added/updated: test_classify_keyboard_response (now
includes fid=1212), test_classify_keyboard_response_legacy_fid_1211_no_longer_matches,
test_classify_keyboard_response_no_fid_no_longer_matches, and
test_classify_keyboard_response_priority_over_other_p2p_meaning.

Doc updates: crates/cmagent-channels/src/lunkr/p2p.rs module
header (fid layout table), keyboard.rs module header (wire
format note), and the dispatch comment in lunkr/mod.rs all
describe the new fid layout.

[Unreleased] -- 2026-04-22 to 2026-04-24

Highlight: Unified messaging tools

Replaced the per-channel outbound tool surface (lunkr_send_message,
telegram_send_message, <kind>_search_contacts, ...) with two
action-dispatched tools:

  • messaging_query (Low risk) -- read surface: list_channels,
    describe_channel, search_contacts, list_chats,
    list_messages, download_attachment.
  • messaging_send (Medium risk) -- write surface: send_message,
    send_file, edit_message, delete_message, send_buttons,
    notify, plus platform-specific actions (send_embed,
    open_modal, create_thread, add_friend, accept_friend,
    send_pat).

Channels: Lunkr, Telegram, Slack, Discord, WeChat. The LLM uses
describe_channel to learn what each adapter supports before
attempting a call. Inbound channel sessions are scoped: every action
except notify is restricted to the originating channel.

Design doc: docs/plans/2026-04-24-unified-messaging.md.

Added

  • OutboundChannel::describe() returning a ChannelDescriptor
    capability sheet with structured per-action support objects (e.g.
    send_buttons.wait_response = true). [03c863b]
  • MessagingRegistry keyed by channel kind with shared
    Arc<dyn OutboundChannel> and operator-side ChannelMeta
    (display_name, outbound_enabled, notify_target). [1028495]
  • messaging_send.send_buttons accepts wait_response: true +
    timeout_seconds; Lunkr's p2p bridge backs the blocking flow.
    [6d1db12]
  • messaging_query.search_contacts / list_chats / list_messages
    fully wired through to adapter methods (commit 3/5 had left them
    as not_implemented stubs). [b3f1cdf]
  • Centralised cmagent_channels::outbound::CHANNEL_KINDS constant;
    removed five duplicated copies. [d2e11c1]
  • Channel scope detection in src/infra.rs::build_agent (CLI / TUI
    path) so resuming a lunkr-* session enforces the same scope as
    the gateway path. [d2e11c1]
  • Lunkr clone-role self-send guard (check_self_send) with a clear
    error message; only the operator's exact uid is rejected, not
    every #U recipient. [2d8398c]
  • Discovery wires messaging_query / messaging_send into
    discover_tool_names() so the config wizard and cmagent doctor
    can see them. [48090b0]
  • Design document for the rollout: docs/plans/2026-04-24-unified-messaging.md.
    [1f57984]

Changed

  • Shipped agent profiles migrated: chat adds messaging_query
    (read-only); coding and admin add both tools. [6b545cc]
  • Lunkr / Telegram / Slack / Discord / WeChat registrations now
    prefer the account with allow_outbound_send=true over the first
    enabled account so opt-in always wins regardless of TOML order.
    [d2e11c1, 2d8398c]
  • messaging_send parameters schema documents trigger_id,
    target, message, request_id, wait_response,
    timeout_seconds; ref widened to accept string OR object for
    Lunkr's compound attachment shape. [d2e11c1]
  • Telegram / Slack / Discord / WeChat are registered in the
    messaging registry even without allow_outbound_send so
    inbound-only sessions can still call describe_channel; send
    actions stay gated by outbound_enabled. [d2e11c1]
  • cmagent doctor warns when an agent's explicit tool allowlist
    excludes messaging_send while at least one channel has
    allow_outbound_send=true. [b0b41d7, 6b545cc]

Removed

  • All <kind>_send_message, <kind>_send_file,
    <kind>_search_contacts, <kind>_list_chats,
    <kind>_list_messages, <kind>_notify per-channel tools. [6b545cc]
  • lunkr_download_file (replaced by
    messaging_query.download_attachment). [6b545cc]
  • lunkr_send_keyboard (replaced by
    messaging_send.send_buttons { wait_response: true }). [6d1db12]
  • cmagent-tool::builtin::outbound module (legacy tool factories).
    [6b545cc]
  • Synthesized CHANNEL_OUTBOUND_HINTS.md agent.md section --
    unified tools always register, so describe_channel already
    surfaces availability. [6b545cc]

Other

TUI

  • /status "Core" / "Extended" tool lists word-wrap to terminal
    width via the new [status:list]INDENT|LABEL|items marker.
    Narrow windows no longer drop tools off the right edge. [fe0eebe]
  • Streaming thinking preview collapses embedded newlines and word-
    wraps long lines (CJK and ASCII alike). Continuation lines align
    to the tree gutter. [e5f8632]

Outbound config wizard

  • Enable outbound send? prompt added to both new-account and
    edit-account flows. Toggling off clears stale notify_* keys.
    [283782a]
  • Wizard now lists 5 outbound-capable channels (Lunkr, Telegram,
    Slack, Discord, WeChat) consistently via the central constant.
    [283782a, d2e11c1]
  • Workspace prompt in channel edit is optional rather than
    required (previously refused to advance with an empty value).
    [ba4076c]

Lunkr

  • Image attachments are downloaded inline into ChannelEvent.images
    so vision-capable providers see them natively. [5a815c7]
  • Downloaded images persist under the workspace .cmagent/dl/lunkr/
    tree, namespaced by account so multiple accounts don't collide.
    [1f24e36]

Vision

  • Native-first fallback: when a provider exposes vision, images
    ride along in ChatMessage.images; otherwise the vision tool
    is registered as a fallback. Telegram image attachments are now
    preserved end-to-end. [d290408]

Doctor

  • Detects legacy [security] schema (autonomy, max_tool_risk,
    separate path fields) in user configs and points to the new
    field names. [74016f0]

[Unreleased] -- 2026-04-21 to 2026-04-22 (security model refactor)

Three-phase refactor of the agent security schema. Final shape is
documented in docs/security-model.md.

Changed

  • max_tool_risk -> max_skill_risk (gates skills + MCPs only,
    not the tool allowlist). Path fields (allowed_paths,
    workspace.extra_dirs, sandbox_extra_read_paths,
    sandbox_extra_write_paths) consolidated into a single
    extra_dirs = [{ path, mode }] list. [d3319df]
  • Skill / MCP filtering enforces the risk ceiling at runtime
    rather than only at registration time. [16a72b2]
  • autonomy -> prompt_threshold with three values: medium
    (ask for medium+high risk), high (ask for high only), never
    (autonomous). [87aa5e0]

Added

  • docs/security-model.md: full description of the four-layer
    security model and the runtime gate. [619867a]
  • cargo fmt --all baseline applied across the workspace. [422ca8b]
  • Hermes Agent research notes; Python cache added to .gitignore.
    [6db384b]