mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-15 00:17:27 +00:00
fix(channels): announce tools before execution
This commit is contained in:
1 parent
0e980ffc76
commit
a8afed2a81
22 files changed
+454
-165
No files matched your search
@@ -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.
|
||||
|
||||
|
||||
@@ -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**
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+33
-31
@@ -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.",
|
||||
|
||||
+33
-31
@@ -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.",
|
||||
|
||||
+33
-31
@@ -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% 的迭代预算。请考虑本次会话中的模式是否值得保存为技能。",
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
|
||||
+39
-33
@@ -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"
|
||||
)
|
||||
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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ị)" },
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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 = 共享主网关端口(推荐)" },
|
||||
|
||||
@@ -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": "拟人化频道投递",
|
||||
|
||||
@@ -33,9 +33,9 @@
|
||||
"toolStatus": "执行时显示工具名称",
|
||||
"toolStatusHint": "Agent运行时显示当前工具名称。",
|
||||
"toolStatusInfo": "Agent运行时在流式预览中显示工具名称。",
|
||||
"blockReply": "工具调用时显示中间文本",
|
||||
"blockReplyHint": "工具调用过程中流式传输部分助手文本。",
|
||||
"blockReplyInfo": "工具执行期间向用户流式传输中间文本。",
|
||||
"blockReply": "工具调用前后显示中间文本",
|
||||
"blockReplyHint": "在工具运行前或运行时发送工具公告和部分助手文本。",
|
||||
"blockReplyInfo": "工具执行前或执行期间向用户发送中间文本。",
|
||||
"intentClassify": "意图分类",
|
||||
"intentClassifyHint": "在路由到Agent之前分类用户意图。",
|
||||
"intentClassifyInfo": "消息被分类以优化Agent路由和行为。"
|
||||
|
||||
Reference in new issue
Block a user