From a8afed2a81556ef3994c69d27706aee6528a29cc Mon Sep 17 00:00:00 2001 From: Goon Date: Sun, 31 May 2026 19:24:12 +0700 Subject: [PATCH] fix(channels): announce tools before execution --- docs/05-channels-messaging.md | 3 +- docs/project-changelog.md | 18 +++ internal/agent/loop_pipeline_adapter.go | 27 +++-- .../channels/chat_behavior_events_test.go | 49 ++++++++ internal/channels/events.go | 4 +- internal/i18n/catalog_en.go | 64 ++++++----- internal/i18n/catalog_vi.go | 64 ++++++----- internal/i18n/catalog_zh.go | 64 ++++++----- internal/i18n/keys.go | 4 + internal/pipeline/deps.go | 9 +- internal/pipeline/stages_test.go | 106 ++++++++++++++++++ internal/pipeline/think_stage.go | 99 +++++++++++++++- pkg/protocol/events.go | 72 ++++++------ ui/web/src/i18n/locales/en/channels.json | 2 +- ui/web/src/i18n/locales/en/config.json | 4 +- .../src/i18n/locales/en/system-settings.json | 6 +- ui/web/src/i18n/locales/vi/channels.json | 2 +- ui/web/src/i18n/locales/vi/config.json | 4 +- .../src/i18n/locales/vi/system-settings.json | 6 +- ui/web/src/i18n/locales/zh/channels.json | 2 +- ui/web/src/i18n/locales/zh/config.json | 4 +- .../src/i18n/locales/zh/system-settings.json | 6 +- 22 files changed, 454 insertions(+), 165 deletions(-) diff --git a/docs/05-channels-messaging.md b/docs/05-channels-messaging.md index 56eeca4f..e7ca9d0d 100644 --- a/docs/05-channels-messaging.md +++ b/docs/05-channels-messaging.md @@ -78,7 +78,8 @@ Normal channel messages pass through the shared inbound debouncer before agent e - `quick_ack.mode = "llm_generated"` uses the existing main-turn `block.reply` event as the natural progress response for non-streaming channel runs. It does not make a separate LLM call. - `quick_ack.templates` are fallback messages. In generated mode, the first template is sent only if no generated `block.reply` arrives before `quick_ack.min_delay_ms`. - `quick_ack.mode = "fixed_template"` preserves the older fixed-template acknowledgement behavior. -- `quick_ack.mode = "off"` disables chat-behavior quick acknowledgement. When explicit `gateway.block_reply` is on, the first pre-tool `block.reply` is treated as the immediate acknowledgement and suppressed; later tool-iteration `block.reply` messages can still be delivered as intermediate replies. +- `quick_ack.mode = "off"` disables chat-behavior quick acknowledgement. When explicit `gateway.block_reply` is on, the first generic pre-tool `block.reply` is treated as the immediate acknowledgement and suppressed; deterministic tool announcements still deliver before tool execution because they are part of Intermediate Replies. +- If a model requests tools without assistant text, or with text that does not name the tools, the pipeline emits a safe `tool_announcement` fallback using only sanitized tool names. Tool arguments are never included in this message. - `final_split` splits long final text replies into a bounded number of paragraph messages. - Per-channel `chat_behavior` overrides inherit the gateway config unless a field is explicitly set. diff --git a/docs/project-changelog.md b/docs/project-changelog.md index 337b0f4f..fe0bfbd5 100644 --- a/docs/project-changelog.md +++ b/docs/project-changelog.md @@ -6,6 +6,24 @@ Significant changes, features, and fixes in reverse chronological order. ## 2026-05-31 +### Tool-call announcements + +**Fixes** + +- Intermediate Replies now guarantees a pre-tool announcement when the model + requests tools without assistant text, or with text that does not name the + tools. The fallback uses sanitized tool names only and is tagged as + `tool_announcement`. +- Quick acknowledgement off still suppresses generic first acknowledgements, + but no longer suppresses explicit tool announcements. + +**Tests** + +- Added pipeline coverage for empty-content tool calls and channel coverage for + `tool_announcement` delivery when Quick acknowledgement is off. + +--- + ### Channel intermediate reply gating **Fixes** diff --git a/internal/agent/loop_pipeline_adapter.go b/internal/agent/loop_pipeline_adapter.go index b4480d5a..1b0ccba8 100644 --- a/internal/agent/loop_pipeline_adapter.go +++ b/internal/agent/loop_pipeline_adapter.go @@ -51,6 +51,22 @@ func (l *Loop) buildPipelineDeps(req *RunRequest, bridgeRS *runState) pipeline.P } cb := l.pipelineCallbacks(req, bridgeRS) + emitBlockReply := func(content, source string) { + sanitized := SanitizeAssistantContent(content) + if sanitized == "" || IsSilentReply(sanitized) { + return + } + payload := map[string]string{"content": sanitized} + if source != "" { + payload["source"] = source + } + cb.emitRun(AgentEvent{ + Type: protocol.AgentEventBlockReply, + AgentID: l.id, + RunID: req.RunID, + Payload: payload, + }) + } return pipeline.PipelineDeps{ TokenCounter: tokencount.NewTiktokenCounter(), @@ -105,16 +121,9 @@ func (l *Loop) buildPipelineDeps(req *RunRequest, bridgeRS *runState) pipeline.P CallLLM: cb.callLLM, UniqueToolCallIDs: uniquifyToolCallIDs, EmitBlockReply: func(content string) { - sanitized := SanitizeAssistantContent(content) - if sanitized != "" && !IsSilentReply(sanitized) { - cb.emitRun(AgentEvent{ - Type: protocol.AgentEventBlockReply, - AgentID: l.id, - RunID: req.RunID, - Payload: map[string]string{"content": sanitized}, - }) - } + emitBlockReply(content, "") }, + EmitBlockReplyWithSource: emitBlockReply, // Prune callbacks PruneMessages: cb.pruneMessages, diff --git a/internal/channels/chat_behavior_events_test.go b/internal/channels/chat_behavior_events_test.go index c60c573e..869405d1 100644 --- a/internal/channels/chat_behavior_events_test.go +++ b/internal/channels/chat_behavior_events_test.go @@ -272,3 +272,52 @@ func TestHandleAgentEvent_QuickAckDisabledSuppressesInitialExplicitBlockReply(t }) } } + +func TestHandleAgentEvent_ToolAnnouncementBypassesInitialQuickAckSuppression(t *testing.T) { + for _, tc := range []struct { + name string + quick ResolvedQuickAckConfig + }{ + { + name: "enabled_false", + quick: ResolvedQuickAckConfig{Enabled: false, Templates: []string{"Fallback."}}, + }, + { + name: "mode_off", + quick: ResolvedQuickAckConfig{Enabled: true, Mode: QuickAckModeOff, Templates: []string{"Fallback."}}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + behavior := ResolvedChatBehavior{ + Enabled: true, + QuickAck: tc.quick, + } + + mb := bus.New() + mgr := NewManager(mb) + mgr.RegisterChannel("test", &chatBehaviorTestChannel{name: "test"}) + mgr.RegisterRunWithBehavior("run-1", "test", "chat-1", "msg-1", nil, uuid.Nil, false, true, true, behavior) + + mgr.HandleAgentEvent(protocol.AgentEventRunStarted, "run-1", nil) + mgr.HandleAgentEvent(protocol.AgentEventBlockReply, "run-1", map[string]string{ + "content": "Tôi sẽ dùng `skill_search` để xử lý bước tiếp theo.", + "source": protocol.BlockReplySourceToolAnnouncement, + }) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + got, ok := mb.SubscribeOutbound(ctx) + if !ok { + t.Fatal("expected tool announcement outbound message") + } + if got.Content != "Tôi sẽ dùng `skill_search` để xử lý bước tiếp theo." { + t.Fatalf("tool announcement content = %q", got.Content) + } + + delivered, last := mgr.InterimDeliverySnapshot("run-1") + if delivered != 1 || last != got.Content { + t.Fatalf("interim delivery snapshot = (%d, %q), want delivered announcement", delivered, last) + } + }) + } +} diff --git a/internal/channels/events.go b/internal/channels/events.go index d36c1223..a7d1b1ac 100644 --- a/internal/channels/events.go +++ b/internal/channels/events.go @@ -278,6 +278,7 @@ func (m *Manager) HandleAgentEvent(eventType, runID string, payload any) { if content == "" { return } + source := extractPayloadString(payload, "source") rc.mu.Lock() streaming := rc.Streaming rc.blockReplySeen++ @@ -293,7 +294,8 @@ func (m *Manager) HandleAgentEvent(eventType, runID string, payload any) { if !blockReplyEnabled && !generatedProgress { return } - if isInitialBlockReply && blockReplyEnabled && !generatedProgress && ShouldSuppressInitialBlockReply(chatBehavior, streaming) { + isToolAnnouncement := source == protocol.BlockReplySourceToolAnnouncement + if isInitialBlockReply && !isToolAnnouncement && blockReplyEnabled && !generatedProgress && ShouldSuppressInitialBlockReply(chatBehavior, streaming) { return } diff --git a/internal/i18n/catalog_en.go b/internal/i18n/catalog_en.go index 60943dff..37c11706 100644 --- a/internal/i18n/catalog_en.go +++ b/internal/i18n/catalog_en.go @@ -177,37 +177,39 @@ func init() { MsgInvalidProviderOrModel: "invalid provider or model", // Builtin tool descriptions - MsgToolReadFile: "Read the contents of a file from the agent's workspace by path", - MsgToolWriteFile: "Write content to a file in the workspace, creating directories as needed", - MsgToolListFiles: "List files and directories in a given path within the workspace", - MsgToolEdit: "Apply targeted search-and-replace edits to existing files without rewriting the entire file", - MsgToolExec: "Execute a shell command in the workspace and return stdout/stderr", - MsgToolWebSearch: "Search the web for information using a search engine (Brave or DuckDuckGo)", - MsgToolWebFetch: "Fetch a web page or API endpoint and extract its text content", - MsgToolMemorySearch: "Search through the agent's long-term memory using semantic similarity", - MsgToolMemoryGet: "Retrieve a specific memory document by its file path", - MsgToolKGSearch: "Search entities, relationships, and observations in the agent's knowledge graph", - MsgToolReadImage: "Analyze images using a vision-capable LLM provider", - MsgToolReadDocument: "Analyze documents (PDF, Word, Excel, PowerPoint, CSV, etc.) using a document-capable LLM provider", - MsgToolCreateImage: "Generate images from text prompts using an image generation provider", - MsgToolReadAudio: "Analyze audio files (speech, music, sounds) using an audio-capable LLM provider", - MsgToolReadVideo: "Analyze video files using a video-capable LLM provider", - MsgToolCreateVideo: "Generate videos from text descriptions using AI", - MsgToolCreateAudio: "Generate music or sound effects from text descriptions using AI", - MsgToolTTS: "Convert text to natural-sounding speech audio", - MsgToolBrowser: "Automate browser interactions: navigate pages, click elements, fill forms, take screenshots", - MsgToolSessionsList: "List active chat sessions across all channels", - MsgToolSessionStatus: "Get the current status and metadata of a specific chat session", - MsgToolSessionsHistory: "Retrieve the message history of a specific chat session", - MsgToolSessionsSend: "Send a message to an active chat session on behalf of the agent", - MsgToolMessage: "Send a proactive message to a user on a connected channel (Telegram, Discord, etc.)", - MsgToolCron: "Schedule or manage recurring tasks using cron expressions, at-times, or intervals", - MsgToolSpawn: "Spawn a subagent for background work or delegate a task to a linked agent", - MsgToolSkillSearch: "Search for available skills by keyword or description to find relevant capabilities", - MsgToolUseSkill: "Activate a skill to use its specialized capabilities (tracing marker)", - MsgToolSkillManage: "Create, patch, or delete skills from conversation experience", - MsgToolPublishSkill: "Register a skill directory in the system database, making it discoverable", - MsgToolTeamTasks: "View, create, update, and complete tasks on the team task board", + MsgToolReadFile: "Read the contents of a file from the agent's workspace by path", + MsgToolWriteFile: "Write content to a file in the workspace, creating directories as needed", + MsgToolListFiles: "List files and directories in a given path within the workspace", + MsgToolEdit: "Apply targeted search-and-replace edits to existing files without rewriting the entire file", + MsgToolExec: "Execute a shell command in the workspace and return stdout/stderr", + MsgToolWebSearch: "Search the web for information using a search engine (Brave or DuckDuckGo)", + MsgToolWebFetch: "Fetch a web page or API endpoint and extract its text content", + MsgToolMemorySearch: "Search through the agent's long-term memory using semantic similarity", + MsgToolMemoryGet: "Retrieve a specific memory document by its file path", + MsgToolKGSearch: "Search entities, relationships, and observations in the agent's knowledge graph", + MsgToolReadImage: "Analyze images using a vision-capable LLM provider", + MsgToolReadDocument: "Analyze documents (PDF, Word, Excel, PowerPoint, CSV, etc.) using a document-capable LLM provider", + MsgToolCreateImage: "Generate images from text prompts using an image generation provider", + MsgToolReadAudio: "Analyze audio files (speech, music, sounds) using an audio-capable LLM provider", + MsgToolReadVideo: "Analyze video files using a video-capable LLM provider", + MsgToolCreateVideo: "Generate videos from text descriptions using AI", + MsgToolCreateAudio: "Generate music or sound effects from text descriptions using AI", + MsgToolTTS: "Convert text to natural-sounding speech audio", + MsgToolBrowser: "Automate browser interactions: navigate pages, click elements, fill forms, take screenshots", + MsgToolSessionsList: "List active chat sessions across all channels", + MsgToolSessionStatus: "Get the current status and metadata of a specific chat session", + MsgToolSessionsHistory: "Retrieve the message history of a specific chat session", + MsgToolSessionsSend: "Send a message to an active chat session on behalf of the agent", + MsgToolMessage: "Send a proactive message to a user on a connected channel (Telegram, Discord, etc.)", + MsgToolCron: "Schedule or manage recurring tasks using cron expressions, at-times, or intervals", + MsgToolSpawn: "Spawn a subagent for background work or delegate a task to a linked agent", + MsgToolSkillSearch: "Search for available skills by keyword or description to find relevant capabilities", + MsgToolUseSkill: "Activate a skill to use its specialized capabilities (tracing marker)", + MsgToolSkillManage: "Create, patch, or delete skills from conversation experience", + MsgToolPublishSkill: "Register a skill directory in the system database, making it discoverable", + MsgToolTeamTasks: "View, create, update, and complete tasks on the team task board", + MsgToolAnnouncementSingle: "I'll use %s to handle the next step.", + MsgToolAnnouncementMulti: "I'll use %s to handle the next step.", MsgSkillNudgePostscript: "This task involved several steps. Want me to save the process as a reusable skill? Reply **\"save as skill\"** or **\"skip\"**.", MsgSkillNudge70Pct: "[System] You are at 70% of your iteration budget. Consider whether any patterns from this session would make a good skill.", diff --git a/internal/i18n/catalog_vi.go b/internal/i18n/catalog_vi.go index df769c49..194834be 100644 --- a/internal/i18n/catalog_vi.go +++ b/internal/i18n/catalog_vi.go @@ -177,37 +177,39 @@ func init() { MsgInvalidProviderOrModel: "provider hoặc model không hợp lệ", // Mô tả công cụ tích hợp - MsgToolReadFile: "Đọc nội dung tệp từ workspace của agent theo đường dẫn", - MsgToolWriteFile: "Ghi nội dung vào tệp trong workspace, tự động tạo thư mục nếu cần", - MsgToolListFiles: "Liệt kê tệp và thư mục trong đường dẫn chỉ định", - MsgToolEdit: "Chỉnh sửa tệp bằng cách tìm và thay thế đoạn văn bản cụ thể", - MsgToolExec: "Thực thi lệnh shell trong workspace và trả về kết quả", - MsgToolWebSearch: "Tìm kiếm thông tin trên web bằng công cụ tìm kiếm (Brave hoặc DuckDuckGo)", - MsgToolWebFetch: "Tải trang web hoặc API endpoint và trích xuất nội dung văn bản", - MsgToolMemorySearch: "Tìm kiếm trong bộ nhớ dài hạn của agent bằng độ tương đồng ngữ nghĩa", - MsgToolMemoryGet: "Lấy tài liệu bộ nhớ cụ thể theo đường dẫn tệp", - MsgToolKGSearch: "Tìm kiếm thực thể, quan hệ và ghi chú trong đồ thị tri thức của agent", - MsgToolReadImage: "Phân tích hình ảnh bằng nhà cung cấp LLM có khả năng nhìn", - MsgToolReadDocument: "Phân tích tài liệu (PDF, Word, Excel, PowerPoint, CSV, v.v.) bằng LLM", - MsgToolCreateImage: "Tạo hình ảnh từ mô tả văn bản bằng nhà cung cấp tạo ảnh AI", - MsgToolReadAudio: "Phân tích tệp âm thanh (giọng nói, nhạc, âm thanh) bằng LLM", - MsgToolReadVideo: "Phân tích tệp video bằng nhà cung cấp LLM có khả năng xử lý video", - MsgToolCreateVideo: "Tạo video từ mô tả văn bản bằng AI", - MsgToolCreateAudio: "Tạo nhạc hoặc hiệu ứng âm thanh từ mô tả văn bản bằng AI", - MsgToolTTS: "Chuyển văn bản thành giọng nói tự nhiên", - MsgToolBrowser: "Tự động hóa trình duyệt: điều hướng trang, click, điền form, chụp ảnh màn hình", - MsgToolSessionsList: "Liệt kê các phiên chat đang hoạt động trên tất cả kênh", - MsgToolSessionStatus: "Xem trạng thái và thông tin chi tiết của một phiên chat", - MsgToolSessionsHistory: "Xem lịch sử tin nhắn của một phiên chat cụ thể", - MsgToolSessionsSend: "Gửi tin nhắn vào một phiên chat đang hoạt động thay mặt agent", - MsgToolMessage: "Gửi tin nhắn chủ động đến người dùng trên kênh đã kết nối (Telegram, Discord, v.v.)", - MsgToolCron: "Lên lịch hoặc quản lý tác vụ định kỳ bằng biểu thức cron, giờ cố định, hoặc khoảng thời gian", - MsgToolSpawn: "Tạo subagent chạy nền hoặc giao việc cho agent đã liên kết", - MsgToolSkillSearch: "Tìm kiếm kỹ năng có sẵn theo từ khóa hoặc mô tả", - MsgToolUseSkill: "Kích hoạt kỹ năng để sử dụng khả năng chuyên biệt (đánh dấu tracing)", - MsgToolSkillManage: "Tạo, sửa hoặc xóa kỹ năng từ trải nghiệm hội thoại", - MsgToolPublishSkill: "Đăng ký thư mục kỹ năng vào hệ thống, cho phép tìm kiếm và cấp quyền", - MsgToolTeamTasks: "Xem, tạo, cập nhật và hoàn thành tác vụ trên bảng tác vụ nhóm", + MsgToolReadFile: "Đọc nội dung tệp từ workspace của agent theo đường dẫn", + MsgToolWriteFile: "Ghi nội dung vào tệp trong workspace, tự động tạo thư mục nếu cần", + MsgToolListFiles: "Liệt kê tệp và thư mục trong đường dẫn chỉ định", + MsgToolEdit: "Chỉnh sửa tệp bằng cách tìm và thay thế đoạn văn bản cụ thể", + MsgToolExec: "Thực thi lệnh shell trong workspace và trả về kết quả", + MsgToolWebSearch: "Tìm kiếm thông tin trên web bằng công cụ tìm kiếm (Brave hoặc DuckDuckGo)", + MsgToolWebFetch: "Tải trang web hoặc API endpoint và trích xuất nội dung văn bản", + MsgToolMemorySearch: "Tìm kiếm trong bộ nhớ dài hạn của agent bằng độ tương đồng ngữ nghĩa", + MsgToolMemoryGet: "Lấy tài liệu bộ nhớ cụ thể theo đường dẫn tệp", + MsgToolKGSearch: "Tìm kiếm thực thể, quan hệ và ghi chú trong đồ thị tri thức của agent", + MsgToolReadImage: "Phân tích hình ảnh bằng nhà cung cấp LLM có khả năng nhìn", + MsgToolReadDocument: "Phân tích tài liệu (PDF, Word, Excel, PowerPoint, CSV, v.v.) bằng LLM", + MsgToolCreateImage: "Tạo hình ảnh từ mô tả văn bản bằng nhà cung cấp tạo ảnh AI", + MsgToolReadAudio: "Phân tích tệp âm thanh (giọng nói, nhạc, âm thanh) bằng LLM", + MsgToolReadVideo: "Phân tích tệp video bằng nhà cung cấp LLM có khả năng xử lý video", + MsgToolCreateVideo: "Tạo video từ mô tả văn bản bằng AI", + MsgToolCreateAudio: "Tạo nhạc hoặc hiệu ứng âm thanh từ mô tả văn bản bằng AI", + MsgToolTTS: "Chuyển văn bản thành giọng nói tự nhiên", + MsgToolBrowser: "Tự động hóa trình duyệt: điều hướng trang, click, điền form, chụp ảnh màn hình", + MsgToolSessionsList: "Liệt kê các phiên chat đang hoạt động trên tất cả kênh", + MsgToolSessionStatus: "Xem trạng thái và thông tin chi tiết của một phiên chat", + MsgToolSessionsHistory: "Xem lịch sử tin nhắn của một phiên chat cụ thể", + MsgToolSessionsSend: "Gửi tin nhắn vào một phiên chat đang hoạt động thay mặt agent", + MsgToolMessage: "Gửi tin nhắn chủ động đến người dùng trên kênh đã kết nối (Telegram, Discord, v.v.)", + MsgToolCron: "Lên lịch hoặc quản lý tác vụ định kỳ bằng biểu thức cron, giờ cố định, hoặc khoảng thời gian", + MsgToolSpawn: "Tạo subagent chạy nền hoặc giao việc cho agent đã liên kết", + MsgToolSkillSearch: "Tìm kiếm kỹ năng có sẵn theo từ khóa hoặc mô tả", + MsgToolUseSkill: "Kích hoạt kỹ năng để sử dụng khả năng chuyên biệt (đánh dấu tracing)", + MsgToolSkillManage: "Tạo, sửa hoặc xóa kỹ năng từ trải nghiệm hội thoại", + MsgToolPublishSkill: "Đăng ký thư mục kỹ năng vào hệ thống, cho phép tìm kiếm và cấp quyền", + MsgToolTeamTasks: "Xem, tạo, cập nhật và hoàn thành tác vụ trên bảng tác vụ nhóm", + MsgToolAnnouncementSingle: "Tôi sẽ dùng %s để xử lý bước tiếp theo.", + MsgToolAnnouncementMulti: "Tôi sẽ dùng %s để xử lý bước tiếp theo.", MsgSkillNudgePostscript: "Tác vụ này cần nhiều bước. Bạn muốn tôi lưu quy trình này thành kỹ năng tái sử dụng không? Trả lời **\"lưu kỹ năng\"** hoặc **\"bỏ qua\"**.", MsgSkillNudge70Pct: "[System] Bạn đã dùng 70% ngân sách vòng lặp. Cân nhắc xem các mẫu trong phiên này có nên lưu thành kỹ năng không.", diff --git a/internal/i18n/catalog_zh.go b/internal/i18n/catalog_zh.go index 2053cfa9..c1989d84 100644 --- a/internal/i18n/catalog_zh.go +++ b/internal/i18n/catalog_zh.go @@ -177,37 +177,39 @@ func init() { MsgInvalidProviderOrModel: "provider 或 model 无效", // 内置工具描述 - MsgToolReadFile: "按路径读取代理工作区中的文件内容", - MsgToolWriteFile: "将内容写入工作区中的文件,自动创建所需目录", - MsgToolListFiles: "列出工作区指定路径中的文件和目录", - MsgToolEdit: "通过查找和替换对现有文件进行定向编辑,无需重写整个文件", - MsgToolExec: "在工作区中执行 shell 命令并返回标准输出/错误", - MsgToolWebSearch: "使用搜索引擎(Brave 或 DuckDuckGo)在网络上搜索信息", - MsgToolWebFetch: "获取网页或 API 端点并提取其文本内容", - MsgToolMemorySearch: "使用语义相似度搜索代理的长期记忆", - MsgToolMemoryGet: "按文件路径检索特定的记忆文档", - MsgToolKGSearch: "搜索代理知识图谱中的实体、关系和观察记录", - MsgToolReadImage: "使用具有视觉能力的 LLM 提供商分析图像", - MsgToolReadDocument: "使用 LLM 分析文档(PDF、Word、Excel、PowerPoint、CSV 等)", - MsgToolCreateImage: "使用 AI 图像生成提供商从文本提示生成图像", - MsgToolReadAudio: "使用具有音频能力的 LLM 分析音频文件(语音、音乐、声音)", - MsgToolReadVideo: "使用具有视频能力的 LLM 分析视频文件", - MsgToolCreateVideo: "使用 AI 从文本描述生成视频", - MsgToolCreateAudio: "使用 AI 从文本描述生成音乐或音效", - MsgToolTTS: "将文本转换为自然语音音频", - MsgToolBrowser: "自动化浏览器交互:导航页面、点击元素、填写表单、截图", - MsgToolSessionsList: "列出所有渠道中的活跃聊天会话", - MsgToolSessionStatus: "获取特定聊天会话的当前状态和元数据", - MsgToolSessionsHistory: "检索特定聊天会话的消息历史", - MsgToolSessionsSend: "代理代表向活跃聊天会话发送消息", - MsgToolMessage: "在已连接的渠道(Telegram、Discord 等)上向用户主动发送消息", - MsgToolCron: "使用 cron 表达式、定时或间隔来调度或管理定期任务", - MsgToolSpawn: "创建子代理执行后台工作或将任务委派给已链接的代理", - MsgToolSkillSearch: "按关键字或描述搜索可用技能以查找相关功能", - MsgToolUseSkill: "激活技能以使用其专门功能(追踪标记)", - MsgToolSkillManage: "从对话经验中创建、修补或删除技能", - MsgToolPublishSkill: "将技能目录注册到系统数据库中,使其可被发现和授权", - MsgToolTeamTasks: "查看、创建、更新和完成团队任务板上的任务", + MsgToolReadFile: "按路径读取代理工作区中的文件内容", + MsgToolWriteFile: "将内容写入工作区中的文件,自动创建所需目录", + MsgToolListFiles: "列出工作区指定路径中的文件和目录", + MsgToolEdit: "通过查找和替换对现有文件进行定向编辑,无需重写整个文件", + MsgToolExec: "在工作区中执行 shell 命令并返回标准输出/错误", + MsgToolWebSearch: "使用搜索引擎(Brave 或 DuckDuckGo)在网络上搜索信息", + MsgToolWebFetch: "获取网页或 API 端点并提取其文本内容", + MsgToolMemorySearch: "使用语义相似度搜索代理的长期记忆", + MsgToolMemoryGet: "按文件路径检索特定的记忆文档", + MsgToolKGSearch: "搜索代理知识图谱中的实体、关系和观察记录", + MsgToolReadImage: "使用具有视觉能力的 LLM 提供商分析图像", + MsgToolReadDocument: "使用 LLM 分析文档(PDF、Word、Excel、PowerPoint、CSV 等)", + MsgToolCreateImage: "使用 AI 图像生成提供商从文本提示生成图像", + MsgToolReadAudio: "使用具有音频能力的 LLM 分析音频文件(语音、音乐、声音)", + MsgToolReadVideo: "使用具有视频能力的 LLM 分析视频文件", + MsgToolCreateVideo: "使用 AI 从文本描述生成视频", + MsgToolCreateAudio: "使用 AI 从文本描述生成音乐或音效", + MsgToolTTS: "将文本转换为自然语音音频", + MsgToolBrowser: "自动化浏览器交互:导航页面、点击元素、填写表单、截图", + MsgToolSessionsList: "列出所有渠道中的活跃聊天会话", + MsgToolSessionStatus: "获取特定聊天会话的当前状态和元数据", + MsgToolSessionsHistory: "检索特定聊天会话的消息历史", + MsgToolSessionsSend: "代理代表向活跃聊天会话发送消息", + MsgToolMessage: "在已连接的渠道(Telegram、Discord 等)上向用户主动发送消息", + MsgToolCron: "使用 cron 表达式、定时或间隔来调度或管理定期任务", + MsgToolSpawn: "创建子代理执行后台工作或将任务委派给已链接的代理", + MsgToolSkillSearch: "按关键字或描述搜索可用技能以查找相关功能", + MsgToolUseSkill: "激活技能以使用其专门功能(追踪标记)", + MsgToolSkillManage: "从对话经验中创建、修补或删除技能", + MsgToolPublishSkill: "将技能目录注册到系统数据库中,使其可被发现和授权", + MsgToolTeamTasks: "查看、创建、更新和完成团队任务板上的任务", + MsgToolAnnouncementSingle: "我将使用 %s 处理下一步。", + MsgToolAnnouncementMulti: "我将使用 %s 处理下一步。", MsgSkillNudgePostscript: "此任务涉及多个步骤。要我将此过程保存为可重用技能吗?回复 **\"保存技能\"** 或 **\"跳过\"**。", MsgSkillNudge70Pct: "[System] 您已使用 70% 的迭代预算。请考虑本次会话中的模式是否值得保存为技能。", diff --git a/internal/i18n/keys.go b/internal/i18n/keys.go index 4516d15f..34442601 100644 --- a/internal/i18n/keys.go +++ b/internal/i18n/keys.go @@ -252,6 +252,10 @@ const ( MsgSkillNudge70Pct = "skill.nudge_70_pct" MsgSkillNudge90Pct = "skill.nudge_90_pct" + // Tool progress announcements (user-facing) + MsgToolAnnouncementSingle = "progress.tool_announcement.single" // "I'll use %s to handle the next step." + MsgToolAnnouncementMulti = "progress.tool_announcement.multi" // "I'll use %s to handle the next step." + // --- Tenants --- MsgInvalidRole = "error.invalid_role" // "invalid role: allowed values are owner, admin, operator, member, viewer" diff --git a/internal/pipeline/deps.go b/internal/pipeline/deps.go index 2f713f2f..8ba2d47f 100644 --- a/internal/pipeline/deps.go +++ b/internal/pipeline/deps.go @@ -61,10 +61,11 @@ type PipelineDeps struct { InjectReminders func(ctx context.Context, input *RunInput, msgs []providers.Message) []providers.Message // Think callbacks (ThinkStage) - BuildFilteredTools func(state *RunState) ([]providers.ToolDefinition, error) - CallLLM func(ctx context.Context, state *RunState, req providers.ChatRequest) (*providers.ChatResponse, error) - UniqueToolCallIDs func(calls []providers.ToolCall, runID string, iteration int) []providers.ToolCall - EmitBlockReply func(content string) // emit block.reply for intermediate assistant content + BuildFilteredTools func(state *RunState) ([]providers.ToolDefinition, error) + CallLLM func(ctx context.Context, state *RunState, req providers.ChatRequest) (*providers.ChatResponse, error) + UniqueToolCallIDs func(calls []providers.ToolCall, runID string, iteration int) []providers.ToolCall + EmitBlockReply func(content string) // emit block.reply for intermediate assistant content + EmitBlockReplyWithSource func(content, source string) // emit block.reply with payload source metadata // Prune callbacks (PruneStage) PruneMessages func(msgs []providers.Message, budget int) ([]providers.Message, PruneStats) diff --git a/internal/pipeline/stages_test.go b/internal/pipeline/stages_test.go index c9678d2b..a868c476 100644 --- a/internal/pipeline/stages_test.go +++ b/internal/pipeline/stages_test.go @@ -17,7 +17,9 @@ import ( "github.com/nextlevelbuilder/goclaw/internal/config" "github.com/nextlevelbuilder/goclaw/internal/hooks" "github.com/nextlevelbuilder/goclaw/internal/providers" + "github.com/nextlevelbuilder/goclaw/internal/store" "github.com/nextlevelbuilder/goclaw/internal/workspace" + "github.com/nextlevelbuilder/goclaw/pkg/protocol" ) // --- shared test helpers --- @@ -123,6 +125,110 @@ func TestThinkStage_WithToolCalls_ReturnsContinue(t *testing.T) { } } +func TestThinkStage_EmptyToolCallContent_EmitsToolAnnouncement(t *testing.T) { + t.Parallel() + var gotContent, gotSource string + deps := &PipelineDeps{ + Config: PipelineConfig{MaxIterations: 10, MaxTokens: 1000}, + CallLLM: func(_ context.Context, _ *RunState, _ providers.ChatRequest) (*providers.ChatResponse, error) { + return &providers.ChatResponse{ + FinishReason: "tool_calls", + ToolCalls: []providers.ToolCall{{ + ID: "tc1", + Name: "search\n`secret`", + Arguments: map[string]any{"api_key": "must-not-leak"}, + }}, + }, nil + }, + EmitBlockReplyWithSource: func(content, source string) { + gotContent = content + gotSource = source + }, + } + stage := NewThinkStage(deps) + state := defaultState() + + err := stage.Execute(context.Background(), state) + if err != nil { + t.Fatalf("Execute() error: %v", err) + } + if gotSource != protocol.BlockReplySourceToolAnnouncement { + t.Fatalf("block reply source = %q, want %q", gotSource, protocol.BlockReplySourceToolAnnouncement) + } + if !strings.Contains(gotContent, "search secret") { + t.Fatalf("announcement content = %q, want sanitized tool name", gotContent) + } + if strings.Contains(gotContent, "must-not-leak") || strings.Contains(gotContent, "api_key") { + t.Fatalf("announcement leaked tool arguments: %q", gotContent) + } +} + +func TestThinkStage_GeneratedToolCallContent_AppendsMissingToolAnnouncement(t *testing.T) { + t.Parallel() + var gotContent, gotSource string + deps := &PipelineDeps{ + Config: PipelineConfig{MaxIterations: 10, MaxTokens: 1000}, + CallLLM: func(_ context.Context, _ *RunState, _ providers.ChatRequest) (*providers.ChatResponse, error) { + return &providers.ChatResponse{ + Content: "Tôi sẽ tìm trong kho kỹ năng trước.", + FinishReason: "tool_calls", + ToolCalls: []providers.ToolCall{{ID: "tc1", Name: "skill_search"}}, + }, nil + }, + EmitBlockReplyWithSource: func(content, source string) { + gotContent = content + gotSource = source + }, + } + stage := NewThinkStage(deps) + state := defaultState() + ctx := store.WithLocale(context.Background(), "vi") + + err := stage.Execute(ctx, state) + if err != nil { + t.Fatalf("Execute() error: %v", err) + } + want := "Tôi sẽ tìm trong kho kỹ năng trước.\n\nTôi sẽ dùng `skill_search` để xử lý bước tiếp theo." + if gotContent != want { + t.Fatalf("block reply content = %q", gotContent) + } + if gotSource != protocol.BlockReplySourceToolAnnouncement { + t.Fatalf("block reply source = %q, want %q", gotSource, protocol.BlockReplySourceToolAnnouncement) + } +} + +func TestThinkStage_GeneratedToolCallContent_DoesNotDuplicateNamedTool(t *testing.T) { + t.Parallel() + var gotContent, gotSource string + deps := &PipelineDeps{ + Config: PipelineConfig{MaxIterations: 10, MaxTokens: 1000}, + CallLLM: func(_ context.Context, _ *RunState, _ providers.ChatRequest) (*providers.ChatResponse, error) { + return &providers.ChatResponse{ + Content: "Tôi sẽ dùng `skill_search` để tìm capability phù hợp trước.", + FinishReason: "tool_calls", + ToolCalls: []providers.ToolCall{{ID: "tc1", Name: "skill_search"}}, + }, nil + }, + EmitBlockReplyWithSource: func(content, source string) { + gotContent = content + gotSource = source + }, + } + stage := NewThinkStage(deps) + state := defaultState() + + err := stage.Execute(context.Background(), state) + if err != nil { + t.Fatalf("Execute() error: %v", err) + } + if gotContent != "Tôi sẽ dùng `skill_search` để tìm capability phù hợp trước." { + t.Fatalf("block reply content = %q", gotContent) + } + if gotSource != protocol.BlockReplySourceToolAnnouncement { + t.Fatalf("block reply source = %q, want %q", gotSource, protocol.BlockReplySourceToolAnnouncement) + } +} + func TestThinkStage_Truncation_FirstRetry_AppendsContinueMessage(t *testing.T) { t.Parallel() deps := &PipelineDeps{ diff --git a/internal/pipeline/think_stage.go b/internal/pipeline/think_stage.go index 582a79a2..0dd48135 100644 --- a/internal/pipeline/think_stage.go +++ b/internal/pipeline/think_stage.go @@ -6,7 +6,10 @@ import ( "log/slog" "strings" + "github.com/nextlevelbuilder/goclaw/internal/i18n" "github.com/nextlevelbuilder/goclaw/internal/providers" + "github.com/nextlevelbuilder/goclaw/internal/store" + "github.com/nextlevelbuilder/goclaw/pkg/protocol" ) const maxTruncRetries = 3 @@ -23,7 +26,7 @@ func NewThinkStage(deps *PipelineDeps) *ThinkStage { return &ThinkStage{deps: deps, result: Continue} } -func (s *ThinkStage) Name() string { return "think" } +func (s *ThinkStage) Name() string { return "think" } func (s *ThinkStage) Result() StageResult { return s.result } // Execute builds tools, calls LLM, handles truncation, sets flow control. @@ -145,15 +148,99 @@ func (s *ThinkStage) Execute(ctx context.Context, state *RunState) error { } state.Messages.AppendPending(assistantMsg) - // Emit block.reply for intermediate assistant content during tool iterations. - // Non-streaming channels (Zalo, Discord, WhatsApp) need this for delivery. - if resp.Content != "" && s.deps.EmitBlockReply != nil { - s.deps.EmitBlockReply(resp.Content) - } + s.emitToolIterationBlockReply(ctx, resp) return nil } +func (s *ThinkStage) emitToolIterationBlockReply(ctx context.Context, resp *providers.ChatResponse) { + content := resp.Content + source := protocol.BlockReplySourceLLMProgress + announcement := defaultToolAnnouncement(ctx, resp.ToolCalls) + if strings.TrimSpace(content) == "" { + content = announcement + source = protocol.BlockReplySourceToolAnnouncement + } else if announcement != "" { + if !contentMentionsAnnouncedTools(content, resp.ToolCalls) { + content = strings.TrimRight(content, "\r\n\t ") + "\n\n" + announcement + } + source = protocol.BlockReplySourceToolAnnouncement + } + if strings.TrimSpace(content) == "" { + return + } + if s.deps.EmitBlockReplyWithSource != nil { + s.deps.EmitBlockReplyWithSource(content, source) + return + } + if s.deps.EmitBlockReply != nil { + s.deps.EmitBlockReply(content) + } +} + +func defaultToolAnnouncement(ctx context.Context, calls []providers.ToolCall) string { + names := announcedToolNames(calls) + if len(names) == 0 { + return "" + } + list := strings.Join(names, ", ") + locale := store.LocaleFromContext(ctx) + if len(names) == 1 { + return i18n.T(locale, i18n.MsgToolAnnouncementSingle, list) + } + return i18n.T(locale, i18n.MsgToolAnnouncementMulti, list) +} + +func announcedToolNames(calls []providers.ToolCall) []string { + names := sanitizedAnnouncementToolNames(calls) + for i, name := range names { + names[i] = "`" + name + "`" + } + return names +} + +func contentMentionsAnnouncedTools(content string, calls []providers.ToolCall) bool { + names := sanitizedAnnouncementToolNames(calls) + if len(names) == 0 { + return false + } + lowerContent := strings.ToLower(content) + for _, name := range names { + if !strings.Contains(lowerContent, strings.ToLower(name)) { + return false + } + } + return true +} + +func sanitizedAnnouncementToolNames(calls []providers.ToolCall) []string { + names := make([]string, 0, len(calls)) + seen := make(map[string]struct{}, len(calls)) + for _, call := range calls { + name := sanitizeToolAnnouncementName(call.Name) + if name == "" { + continue + } + if _, ok := seen[name]; ok { + continue + } + seen[name] = struct{}{} + names = append(names, name) + } + return names +} + +func sanitizeToolAnnouncementName(name string) string { + name = strings.TrimSpace(name) + name = strings.NewReplacer("`", "", "\n", " ", "\r", " ", "\t", " ").Replace(name) + name = strings.Join(strings.Fields(name), " ") + runes := []rune(name) + if len(runes) > 64 { + name = string(runes[:64]) + } + return name +} + // maybeInjectNudge injects iteration budget warnings at 70% and 90%. func (s *ThinkStage) maybeInjectNudge(state *RunState) { maxIter := s.deps.Config.MaxIterations diff --git a/pkg/protocol/events.go b/pkg/protocol/events.go index 41889484..ec81ac4c 100644 --- a/pkg/protocol/events.go +++ b/pkg/protocol/events.go @@ -2,23 +2,23 @@ package protocol // WebSocket event names pushed from server to client. const ( - EventAgent = "agent" - EventChat = "chat" - EventHealth = "health" - EventCron = "cron" - EventHeartbeat = "heartbeat" - EventExecApprovalReq = "exec.approval.requested" - EventExecApprovalRes = "exec.approval.resolved" - EventPresence = "presence" - EventTick = "tick" - EventShutdown = "shutdown" - EventNodePairRequested = "node.pair.requested" - EventNodePairResolved = "node.pair.resolved" - EventDevicePairReq = "device.pair.requested" - EventDevicePairRes = "device.pair.resolved" - EventVoicewakeChanged = "voicewake.changed" - EventConnectChallenge = "connect.challenge" - EventTalkMode = "talk.mode" + EventAgent = "agent" + EventChat = "chat" + EventHealth = "health" + EventCron = "cron" + EventHeartbeat = "heartbeat" + EventExecApprovalReq = "exec.approval.requested" + EventExecApprovalRes = "exec.approval.resolved" + EventPresence = "presence" + EventTick = "tick" + EventShutdown = "shutdown" + EventNodePairRequested = "node.pair.requested" + EventNodePairResolved = "node.pair.resolved" + EventDevicePairReq = "device.pair.requested" + EventDevicePairRes = "device.pair.resolved" + EventVoicewakeChanged = "voicewake.changed" + EventConnectChallenge = "connect.challenge" + EventTalkMode = "talk.mode" // Agent summoning events (predefined agent setup via LLM). EventAgentSummoning = "agent.summoning" @@ -38,19 +38,19 @@ const ( EventDelegationAnnounce = "delegation.announce" // Team task lifecycle events. - EventTeamTaskClaimed = "team.task.claimed" - EventTeamTaskCancelled = "team.task.cancelled" - EventTeamTaskFailed = "team.task.failed" - EventTeamTaskReviewed = "team.task.reviewed" - EventTeamTaskApproved = "team.task.approved" - EventTeamTaskRejected = "team.task.rejected" - EventTeamTaskProgress = "team.task.progress" - EventTeamTaskCommented = "team.task.commented" - EventTeamTaskAssigned = "team.task.assigned" - EventTeamTaskDispatched = "team.task.dispatched" - EventTeamTaskUpdated = "team.task.updated" - EventTeamTaskDeleted = "team.task.deleted" - EventTeamTaskStale = "team.task.stale" + EventTeamTaskClaimed = "team.task.claimed" + EventTeamTaskCancelled = "team.task.cancelled" + EventTeamTaskFailed = "team.task.failed" + EventTeamTaskReviewed = "team.task.reviewed" + EventTeamTaskApproved = "team.task.approved" + EventTeamTaskRejected = "team.task.rejected" + EventTeamTaskProgress = "team.task.progress" + EventTeamTaskCommented = "team.task.commented" + EventTeamTaskAssigned = "team.task.assigned" + EventTeamTaskDispatched = "team.task.dispatched" + EventTeamTaskUpdated = "team.task.updated" + EventTeamTaskDeleted = "team.task.deleted" + EventTeamTaskStale = "team.task.stale" EventTeamTaskAttachmentAdded = "team.task.attachment_added" // Emitted when leader starts processing completed team task results (before announce run). @@ -136,9 +136,15 @@ const ( AgentEventActivity = "activity" // agent phase transitions: thinking, tool_exec, compacting ) +// block.reply payload source values. +const ( + BlockReplySourceLLMProgress = "llm_progress" + BlockReplySourceToolAnnouncement = "tool_announcement" +) + // Chat event subtypes (in payload.type) const ( - ChatEventChunk = "chunk" - ChatEventMessage = "message" - ChatEventThinking = "thinking" + ChatEventChunk = "chunk" + ChatEventMessage = "message" + ChatEventThinking = "thinking" ) diff --git a/ui/web/src/i18n/locales/en/channels.json b/ui/web/src/i18n/locales/en/channels.json index 40770a51..cd8cceb3 100644 --- a/ui/web/src/i18n/locales/en/channels.json +++ b/ui/web/src/i18n/locales/en/channels.json @@ -348,7 +348,7 @@ "allow_from": { "label": "Allowed Users" }, "block_reply": { "label": "Block Reply", - "help": "Deliver intermediate text during tool iterations" + "help": "Deliver tool announcements and intermediate text around tool iterations" }, "domain": { "label": "Domain" }, "connection_mode": { diff --git a/ui/web/src/i18n/locales/en/config.json b/ui/web/src/i18n/locales/en/config.json index 03d56b46..dcf569de 100644 --- a/ui/web/src/i18n/locales/en/config.json +++ b/ui/web/src/i18n/locales/en/config.json @@ -124,8 +124,8 @@ "behavior.uxDescription": "High-impact toggles that change how the gateway interacts with users", "behavior.toolStatusHint": "Show real-time tool execution status messages to users during agent runs.", "behavior.toolStatusInfo": "Users will see live updates as the agent reads files, runs commands, and calls APIs.", - "behavior.blockReplyHint": "Send partial assistant text to channels while tools are being invoked.", - "behavior.blockReplyInfo": "Intermediate text is delivered to users during tool execution, not just the final response.", + "behavior.blockReplyHint": "Send tool announcements and partial assistant text to channels before or during tool calls.", + "behavior.blockReplyInfo": "Intermediate text is delivered before or during tool execution, not just the final response.", "behavior.intentClassifyHint": "Classify user intent before routing to reduce unnecessary agent invocations.", "behavior.intentClassifyInfo": "Agent will only be invoked when the classifier detects actionable intent.", "behavior.chatTitle": "Human-like Channel Delivery", diff --git a/ui/web/src/i18n/locales/en/system-settings.json b/ui/web/src/i18n/locales/en/system-settings.json index 911e74b1..cb498267 100644 --- a/ui/web/src/i18n/locales/en/system-settings.json +++ b/ui/web/src/i18n/locales/en/system-settings.json @@ -33,9 +33,9 @@ "toolStatus": "Show tool name during execution", "toolStatusHint": "Display the active tool name while the agent is running.", "toolStatusInfo": "Tool names are shown in streaming preview while the agent runs.", - "blockReply": "Show intermediate text during tool calls", - "blockReplyHint": "Stream partial assistant text while tools are being invoked.", - "blockReplyInfo": "Intermediate text is streamed to users during tool execution.", + "blockReply": "Show intermediate text around tool calls", + "blockReplyHint": "Send tool announcements and partial assistant text before or while tools run.", + "blockReplyInfo": "Intermediate text is delivered to users before or during tool execution.", "intentClassify": "Intent classification", "intentClassifyHint": "Classify user intent before routing to the agent.", "intentClassifyInfo": "Messages are classified to optimize agent routing and behavior." diff --git a/ui/web/src/i18n/locales/vi/channels.json b/ui/web/src/i18n/locales/vi/channels.json index 3513710c..bd0db37c 100644 --- a/ui/web/src/i18n/locales/vi/channels.json +++ b/ui/web/src/i18n/locales/vi/channels.json @@ -309,7 +309,7 @@ "media_max_mb": { "label": "Kích thước media tối đa (MB)" }, "link_preview": { "label": "Xem trước liên kết" }, "allow_from": { "label": "Người dùng được phép" }, - "block_reply": { "label": "Phản hồi khối", "help": "Gửi văn bản trung gian trong quá trình lặp công cụ" }, + "block_reply": { "label": "Phản hồi khối", "help": "Gửi thông báo dùng công cụ và văn bản trung gian quanh các vòng gọi công cụ" }, "domain": { "label": "Tên miền" }, "connection_mode": { "label": "Chế độ kết nối", "help": "WebSocket không cần IP công khai — chỉ kết nối ra ngoài" }, "webhook_port": { "label": "Cổng webhook", "help": "0 = chia sẻ cổng gateway chính (khuyến nghị)" }, diff --git a/ui/web/src/i18n/locales/vi/config.json b/ui/web/src/i18n/locales/vi/config.json index cfd0574b..09e36d4b 100644 --- a/ui/web/src/i18n/locales/vi/config.json +++ b/ui/web/src/i18n/locales/vi/config.json @@ -124,8 +124,8 @@ "behavior.uxDescription": "Các công tắc có tác động lớn thay đổi cách gateway tương tác với người dùng", "behavior.toolStatusHint": "Hiện thông báo trạng thái thực thi công cụ theo thời gian thực cho người dùng trong quá trình chạy agent.", "behavior.toolStatusInfo": "Người dùng sẽ thấy cập nhật trực tiếp khi agent đọc tệp, chạy lệnh và gọi API.", - "behavior.blockReplyHint": "Gửi văn bản phụ trợ cho kênh trong khi công cụ đang được gọi.", - "behavior.blockReplyInfo": "Văn bản trung gian được gửi cho người dùng trong quá trình thực thi công cụ.", + "behavior.blockReplyHint": "Gửi thông báo dùng công cụ và văn bản phụ trợ cho kênh trước hoặc trong khi gọi công cụ.", + "behavior.blockReplyInfo": "Văn bản trung gian được gửi cho người dùng trước hoặc trong quá trình thực thi công cụ.", "behavior.intentClassifyHint": "Phân loại ý định người dùng trước khi định tuyến để giảm các lần gọi agent không cần thiết.", "behavior.intentClassifyInfo": "Agent chỉ được gọi khi bộ phân loại phát hiện ý định có thể thực hiện.", "behavior.chatTitle": "Gửi tin nhắn giống người hơn", diff --git a/ui/web/src/i18n/locales/vi/system-settings.json b/ui/web/src/i18n/locales/vi/system-settings.json index 4100d6ef..fa08b89e 100644 --- a/ui/web/src/i18n/locales/vi/system-settings.json +++ b/ui/web/src/i18n/locales/vi/system-settings.json @@ -33,9 +33,9 @@ "toolStatus": "Hiện tên công cụ khi thực thi", "toolStatusHint": "Hiển thị tên công cụ đang chạy khi agent hoạt động.", "toolStatusInfo": "Tên công cụ được hiện khi agent đang chạy.", - "blockReply": "Hiện văn bản trung gian khi gọi công cụ", - "blockReplyHint": "Gửi nội dung phụ trợ khi công cụ đang được gọi.", - "blockReplyInfo": "Văn bản trung gian được gửi cho người dùng khi thực thi công cụ.", + "blockReply": "Hiện văn bản trung gian quanh lúc gọi công cụ", + "blockReplyHint": "Gửi thông báo dùng công cụ và nội dung phụ trợ trước hoặc trong khi chạy công cụ.", + "blockReplyInfo": "Văn bản trung gian được gửi cho người dùng trước hoặc trong khi thực thi công cụ.", "intentClassify": "Phân loại ý định", "intentClassifyHint": "Phân loại ý định người dùng trước khi chuyển tới agent.", "intentClassifyInfo": "Tin nhắn được phân loại để tối ưu định tuyến agent." diff --git a/ui/web/src/i18n/locales/zh/channels.json b/ui/web/src/i18n/locales/zh/channels.json index 82a9e264..484b7db6 100644 --- a/ui/web/src/i18n/locales/zh/channels.json +++ b/ui/web/src/i18n/locales/zh/channels.json @@ -309,7 +309,7 @@ "media_max_mb": { "label": "最大媒体大小 (MB)" }, "link_preview": { "label": "链接预览" }, "allow_from": { "label": "允许的用户" }, - "block_reply": { "label": "分块回复", "help": "在工具迭代期间发送中间文本" }, + "block_reply": { "label": "分块回复", "help": "在工具迭代前后发送工具公告和中间文本" }, "domain": { "label": "域名" }, "connection_mode": { "label": "连接模式", "help": "WebSocket 无需公网 IP — 仅需出站连接" }, "webhook_port": { "label": "Webhook 端口", "help": "0 = 共享主网关端口(推荐)" }, diff --git a/ui/web/src/i18n/locales/zh/config.json b/ui/web/src/i18n/locales/zh/config.json index e702e38f..4789909c 100644 --- a/ui/web/src/i18n/locales/zh/config.json +++ b/ui/web/src/i18n/locales/zh/config.json @@ -124,8 +124,8 @@ "behavior.uxDescription": "影响网关与用户交互方式的高影响开关", "behavior.toolStatusHint": "在 Agent 运行期间向用户显示实时工具执行状态消息。", "behavior.toolStatusInfo": "当 Agent 读取文件、运行命令和调用 API 时,用户将看到实时更新。", - "behavior.blockReplyHint": "在工具调用过程中向频道发送部分助手文本。", - "behavior.blockReplyInfo": "工具执行期间向用户发送中间文本,而非仅发送最终响应。", + "behavior.blockReplyHint": "在工具调用前或调用过程中向频道发送工具公告和部分助手文本。", + "behavior.blockReplyInfo": "工具执行前或执行期间向用户发送中间文本,而非仅发送最终响应。", "behavior.intentClassifyHint": "在路由之前对用户意图进行分类,以减少不必要的 Agent 调用。", "behavior.intentClassifyInfo": "仅当分类器检测到可执行意图时才调用 Agent。", "behavior.chatTitle": "拟人化频道投递", diff --git a/ui/web/src/i18n/locales/zh/system-settings.json b/ui/web/src/i18n/locales/zh/system-settings.json index 7f951fdc..e987869f 100644 --- a/ui/web/src/i18n/locales/zh/system-settings.json +++ b/ui/web/src/i18n/locales/zh/system-settings.json @@ -33,9 +33,9 @@ "toolStatus": "执行时显示工具名称", "toolStatusHint": "Agent运行时显示当前工具名称。", "toolStatusInfo": "Agent运行时在流式预览中显示工具名称。", - "blockReply": "工具调用时显示中间文本", - "blockReplyHint": "工具调用过程中流式传输部分助手文本。", - "blockReplyInfo": "工具执行期间向用户流式传输中间文本。", + "blockReply": "工具调用前后显示中间文本", + "blockReplyHint": "在工具运行前或运行时发送工具公告和部分助手文本。", + "blockReplyInfo": "工具执行前或执行期间向用户发送中间文本。", "intentClassify": "意图分类", "intentClassifyHint": "在路由到Agent之前分类用户意图。", "intentClassifyInfo": "消息被分类以优化Agent路由和行为。"