From a2aff99cbfcdcba4aaab52f08b364f14a6d0ef80 Mon Sep 17 00:00:00 2001 From: Martin Barth Date: Sat, 28 Mar 2026 13:54:57 +0100 Subject: [PATCH] feat(agent): add silent_processing mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When silent_processing is true in agents.defaults, the agent runs fully on every message (tool calls execute, session is flushed to disk) but suppresses the empty-response fallback when the LLM produces no text. The channel layer already skips sending empty content, so nothing reaches the user. Explicit LLM text responses are sent as normal. Empty assistant messages are not written to session history to avoid provider errors on the next turn. The session itself is always saved so the user message is not lost from history on silent turns. Both the empty-response fallback and the tool-iteration-limit message are suppressed in silent mode — a background observer agent must not surface internal diagnostics to the channel. --- docs/chat-apps.md | 21 ++++++++++++++++++ docs/configuration.md | 51 +++++++++++++++++++++++++++++++++++++++++++ pkg/agent/loop.go | 10 +++++---- 3 files changed, 78 insertions(+), 4 deletions(-) diff --git a/docs/chat-apps.md b/docs/chat-apps.md index 3d01994ff..c25097708 100644 --- a/docs/chat-apps.md +++ b/docs/chat-apps.md @@ -144,6 +144,27 @@ You can also trigger by keyword prefixes (e.g. `!bot`): } ``` +**Optional: Silent observer mode** + +To have the agent silently read all messages (e.g. for memory/context gathering) while only responding when explicitly addressed, use `agents.defaults.silent_processing` without `group_trigger.mention_only`. Setting `mention_only` would drop non-mention messages before the agent sees them, defeating the purpose: + +```json +{ + "agents": { + "defaults": { + "silent_processing": true + } + }, + "channels": { + "discord": { + "allow_from": ["YOUR_USER_ID", "YOUR_SERVER_ID"] + } + } +} +``` + +The agent processes every message (tool calls, memory writes) but sends nothing when the LLM produces no text. Instruct the LLM via `AGENT.md` to only produce text when directly addressed. + **6. Run** ```bash diff --git a/docs/configuration.md b/docs/configuration.md index 9360d3897..035a9397d 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -361,6 +361,57 @@ The `restrict_to_workspace` setting applies consistently across all execution pa All paths share the same workspace restriction — there's no way to bypass the security boundary through subagents or scheduled tasks. +### Agent Response Behavior + +#### `silent_processing` + +When `silent_processing` is `true`, the agent runs fully on every inbound message — tool calls execute, session history is written, memory files are updated — but if the LLM produces no text output, nothing is sent to the channel. The automatic empty-response fallback message is suppressed. + +If the LLM does produce text, it is sent as normal. `silent_processing` only suppresses the fallback, not intentional responses. + +| Option | Default | Description | +|---|---|---| +| `silent_processing` | `false` | Suppress empty-response fallback; send nothing when LLM produces no text | + +**Config:** + +```json +{ + "agents": { + "defaults": { + "silent_processing": true + } + } +} +``` + +**Environment variable:** `PICOCLAW_AGENTS_DEFAULTS_SILENT_PROCESSING=true` + +**Use case — group observer agent:** + +An agent added to a group chat that silently reads every message, uses `write_file` to maintain a memory log, and only responds when explicitly addressed. Do not set `group_trigger.mention_only` — that would drop non-mention messages before the agent sees them, defeating the purpose. Instead, instruct the LLM via `AGENT.md` to only produce text when addressed: + +```json +{ + "agents": { + "defaults": { + "silent_processing": true + } + }, + "channels": { + "telegram": { + "allow_from": ["YOUR_USER_ID", "-YOUR_GROUP_ID"] + } + } +} +``` + +With this setup the agent processes every group message (for memory/analysis). When the LLM produces no text, `silent_processing` suppresses the fallback and nothing is sent. When the LLM does produce text (e.g. when @mentioned), it is sent as normal. + +> **Note:** In silent mode, the tool-iteration-limit message is also suppressed. Background observer agents should not surface internal diagnostics to the channel. + +--- + ### Heartbeat (Periodic Tasks) PicoClaw can perform periodic tasks automatically. Create a `HEARTBEAT.md` file in your workspace: diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 33de89df6..c95621a35 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -2690,10 +2690,12 @@ turnLoop: ts.setPhase(TurnPhaseFinalizing) ts.setFinalContent(finalContent) - if !ts.opts.NoHistory && finalContent != "" { - finalMsg := providers.Message{Role: "assistant", Content: finalContent} - ts.agent.Sessions.AddMessage(ts.sessionKey, finalMsg.Role, finalMsg.Content) - ts.recordPersistedMessage(finalMsg) + if !ts.opts.NoHistory { + if finalContent != "" { + finalMsg := providers.Message{Role: "assistant", Content: finalContent} + ts.agent.Sessions.AddMessage(ts.sessionKey, finalMsg.Role, finalMsg.Content) + ts.recordPersistedMessage(finalMsg) + } if err := ts.agent.Sessions.Save(ts.sessionKey); err != nil { turnStatus = TurnEndStatusError al.emitEvent(