Port the progress streaming code from the fork's 75fdeb0 commit.
The upstream run_tool_call_loop only uses on_delta for final response
streaming, missing real-time feedback during tool execution.
Added progress sends at 4 points in the tool loop:
- "Thinking..." / "Thinking (round N)..." before each LLM call
- "Got N tool call(s) (Xs)" after LLM responds with tool calls
- Tool start: "⏳ tool_name: hint..." before each tool execution
- Tool complete: "✅ tool_name (Xs)" or "❌ tool_name (Xs)" after
Also added DRAFT_CLEAR_SENTINEL handling in the channel draft updater
so progress lines are cleared before the final answer streams in.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Daemon heartbeat and cron tasks called agent::run() which hardcoded
channel_name as "cli" and always created an ApprovalManager, causing
[Y]es / [N]o / [A]lways stdin prompts on the unattended daemon terminal.
Add interactive parameter to agent::run(): CLI passes true (preserving
approval flow), daemon/cron pass false (no ApprovalManager, channel
marked as "daemon").
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Upstream main now derives schemars::JsonSchema on all config structs.
Our HooksConfig and BuiltinHooksConfig were missing it, causing CI
Build (Smoke) failure when the merge commit was compiled.
- C1: Use real tool success boolean instead of starts_with("Error")
heuristic in after_tool_call hook
- C2: Wire HookRunner from config into ChannelRuntimeContext so hooks
actually fire in daemon/channel mode (was hardcoded to None)
- I1: Suppress unused_imports warning on HookHandler public API re-export
- I3: Remove session_memory and boot_script config fields that had no
backing implementation (YAGNI); keep only command_logger which is wired
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Thread Option<&HookRunner> into run_tool_call_loop with hook fire points
for LLM input, before/after tool calls. Add hooks field to
ChannelRuntimeContext for message received/sending interception.
Build HookRunner from config in run_gateway and fire gateway_start.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Address clippy lints (redundant continue, as-cast, match arms, elided
lifetimes, format vs write!) and reformat long cfg attributes and assert
macros to pass `cargo fmt --check` and `cargo clippy -D warnings`.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add comprehensive tool name alias mapping:
- fileread -> file_read
- filewrite -> file_write
- memoryrecall -> memory_recall
- bash/sh/cmd -> shell
- etc.
Apply to all new parsers (XML attribute, Perl, FunctionCall).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add parser for <FunctionCall> style that MiniMax also uses:
<FunctionCall>
file_read
<code>path>/Users/.../file.md</code>
</FunctionCall>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add parsers for two additional tool call formats that MiniMax LLM uses:
- XML attribute style: <minimax:toolcall><invoke name="shell"><parameter name="command">ls</parameter></invoke></minimax:toolcall>
- Perl/hash-ref style: {tool => "shell", args => { --command "ls" }}
Previously these were sent as plain text to Telegram channel instead of
being executed as tool calls.
Also fixes build warnings:
- Add #[allow(unused_imports)] to cost/mod.rs and onboard/mod.rs re-exports
- Change channels::handle_command visibility to pub(crate)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add input_tokens and output_tokens fields to ObserverEvent::LlmResponse
so per-call token data flows through all observer backends. Prometheus
gains three new counters (llm_requests_total, tokens_input_total,
tokens_output_total) for granular token tracking by provider/model.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a lightweight TokenUsage struct to providers::traits with
input_tokens and output_tokens fields. Add usage: Option<TokenUsage>
to ChatResponse and update all construction sites across providers
and agent modules with usage: None.
This is the first step toward capturing token usage data from LLM
API responses. Currently all sites set usage: None — subsequent
commits will parse actual usage from each provider's response format.
- Remove duplicate `chat` method in reliable.rs (E0201)
- Fix `futures` → `futures_util` imports in agent.rs and loop_.rs (E0433)
- Gate PostgresMemory behind `memory-postgres` feature in cli.rs (E0433)
- Fix regex backreference in XML tool parser (unsupported by regex crate)
- Add missing `skills_prompt_mode` argument in test
- Apply rustfmt to files with formatting issues on main
Gateway channels (WhatsApp, Linq, Nextcloud Talk) were returning raw
<tool> tags without executing tools or showing results. The CLI
correctly executed tools and returned results.
Root cause: gateway handlers used run_gateway_chat_with_multimodal which
explicitly disabled tools for simple chat-only mode.
Fix: Create run_gateway_chat_with_tools() which uses process_message()
for full tool support, while keeping run_gateway_chat_simple() for
the webhook endpoint to maintain backward compatibility with tests.
Changes:
- Add run_gateway_chat_with_tools() for channel handlers (uses process_message)
- Keep run_gateway_chat_simple() for webhook endpoint (uses state.provider)
- Remove unused provider_label variables from channel handlers
- Remove unused imports (ChatMessage, ProviderCapabilityError)
- Fix pre-existing test compilation issue (missing SkillsPromptInjectionMode)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove duplicate chat method in ReliableProvider impl (E0201)
The second chat fn (lines 662-769) was an exact duplicate of the
first (lines 540-647) in the same impl block.
- Gate PostgresMemory usage in memory CLI behind memory-postgres feature (E0433)
super::PostgresMemory is only exported when the feature is enabled;
the Postgres match arm now compiles to an explicit bail when the
feature is off.
- Replace utures::future::join_all with utures_util::future::join_all (E0433)
The crate depends on utures-util, not utures. Fixed in both
agent.rs and loop_.rs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ReliableProvider was missing a chat() override, causing it to fall through
to the default Provider::chat() trait implementation. The default
implementation delegates to chat_with_history() which returns a plain
String and wraps it in ChatResponse with tool_calls: Vec::new() — so
native tool calling was completely broken through the retry/failover
wrapper even though the underlying provider properly supports it.
Changes:
- Add chat() with full retry/backoff/failover logic matching existing
chat_with_system(), chat_with_history(), and chat_with_tools() overrides
- Include context_window_exceeded early-exit matching other method patterns
- Add 7 focused tests: delegation with tool calls, retry recovery,
supports_native_tools propagation, aggregated error reporting,
model failover, non-retryable error skip, and system prompt zero-XML
verification
On non-CLI channels (Telegram, Discord, etc.), tools like shell and
file_write cannot receive interactive approval and are auto-denied,
causing the LLM to see confusing error responses and fabricate answers.
Add a new config option `non_cli_excluded_tools` under `[autonomy]`
that removes specified tools from the tool specs sent to the LLM on
non-CLI channels. This prevents the model from attempting tool calls
that would fail, forcing it to use data already in the system prompt.
The change filters tool_specs in run_tool_call_loop when the
excluded_tools parameter is non-empty. CLI channels are unaffected.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When autonomy is set to "supervised", the approval gate only prompted
interactively on CLI. On Telegram and other channels, all tool calls
were silently auto-approved with ApprovalResponse::Yes, including
high-risk tools like shell — completely bypassing supervised mode.
On non-CLI channels where interactive prompting is not possible, deny
tool calls that require approval instead of auto-approving. Users can
expand the auto_approve list in config to explicitly allow specific
tools on non-interactive channels.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
AnthropicProvider declared supports_native_tools() = true but did not
override chat_with_tools(). The default trait implementation drops all
conversation history (sends only system + last user message), breaking
multi-turn conversations on Telegram and other channels.
Changes:
- Override chat_with_tools() in AnthropicProvider: converts OpenAI-format
tool JSON to ToolSpec and delegates to chat() which preserves full
message history
- Skip build_tool_instructions() XML protocol when provider supports
native tools (saves ~12k chars in system prompt)
- Remove duplicate Tool Use Protocol section from build_system_prompt()
for native-tool providers
- Update Your Task section to encourage conversational follow-ups
instead of XML tool_call tags when using native tools
- Add tracing::warn for malformed tool definitions in chat_with_tools
Every user message was auto-saved to memory regardless of length,
flooding the store with trivial entries like "ok", "thanks", "hi".
These noise entries competed with real memories during recall, degrading
relevance — especially with keyword-only search.
Skip auto-saving messages shorter than 20 characters. Applied to both
the channel path (channels/mod.rs) and CLI agent path (agent/loop_.rs).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds section markers and decision-point comments to the three most complex
control-flow modules. Comments explain loop invariants, retry/fallback
strategy, security policy precedence rules, and error handling rationale.
This improves maintainability by making the reasoning behind complex
branches explicit for reviewers and future contributors.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implement chat_with_tools() on CompatibleProvider so OpenAI-compatible
endpoints (OpenRouter, local LLMs, etc.) can use structured tool calling
instead of prompt-injected tool descriptions.
Changes:
- CompatibleProvider: capabilities() reports native_tool_calling, new
chat_with_tools() sends tools in API request and parses tool_calls
from response, chat() bridges to chat_with_tools() when ToolSpecs
are provided
- RouterProvider: chat_with_tools() delegation with model hint resolution
- loop_.rs: expose tools_to_openai_format as pub(crate), add
tools_to_openai_format_from_specs for ToolSpec-based conversion
Adds 9 new tests and updates 1 existing test.
- Add strip_tool_call_tags() to finalize_draft to prevent Markdown
parse failures from tool-call tags reaching Telegram API
- Deduplicate parse_reply_target() call in update_draft (was called
twice, discarding thread_id both times)
- Replace body.as_object_mut().unwrap() mutation with separate
plain_body JSON literal (eliminates unwrap in runtime path)
- Clean up per-chat rate-limit HashMap entry in finalize_draft to
prevent unbounded growth over long uptimes
- Extract magic number 80 to STREAM_CHUNK_MIN_CHARS constant in
agent loop
Previously on_delta sent the entire completed response as a single
message, defeating the purpose of the streaming draft updates. Now
the text is split into ~80-char chunks on whitespace boundaries
(UTF-8 safe via split_inclusive) and sent progressively through the
channel, so Telegram draft edits show text arriving incrementally.
The consumer in process_channel_message already accumulates chunks
and calls update_draft with the full text so far, and Telegram's
rate-limiting (draft_update_interval_ms) throttles editMessageText
calls to avoid API spam.