merge: integrate main into refactor-inbound-context-routing-session

This commit is contained in:
Hoshina
2026-04-13 13:25:07 +08:00
134 changed files with 13291 additions and 1321 deletions
+5 -5
View File
@@ -29,7 +29,7 @@ func (t *EditFileTool) Name() string {
}
func (t *EditFileTool) Description() string {
return "Edit a file by replacing old_text with new_text. The old_text must exist exactly in the file. In `function.arguments`, use \\n for newline and \\\\n for literal backslash-n."
return "Edit a file by replacing old_text with new_text. The old_text must exist exactly in the file. Standard JSON escaping applies: \\n for newline and \\\\n for literal backslash-n."
}
func (t *EditFileTool) Parameters() map[string]any {
@@ -42,11 +42,11 @@ func (t *EditFileTool) Parameters() map[string]any {
},
"old_text": map[string]any{
"type": "string",
"description": "The exact text to find and replace. In `function.arguments`, use \\n for newline and \\\\n for literal backslash-n.",
"description": "The exact text to find and replace. Standard JSON escaping applies: \\n for newline and \\\\n for literal backslash-n.",
},
"new_text": map[string]any{
"type": "string",
"description": "The text to replace with. In `function.arguments`, use \\n for newline and \\\\n for literal backslash-n.",
"description": "The text to replace with. Standard JSON escaping applies: \\n for newline and \\\\n for literal backslash-n.",
},
},
"required": []string{"path", "old_text", "new_text"},
@@ -92,7 +92,7 @@ func (t *AppendFileTool) Name() string {
}
func (t *AppendFileTool) Description() string {
return "Append content to the end of a file. In `function.arguments`, use \\n for newline and \\\\n for literal backslash-n."
return "Append content to the end of a file. Standard JSON escaping applies: \\n for newline and \\\\n for literal backslash-n."
}
func (t *AppendFileTool) Parameters() map[string]any {
@@ -105,7 +105,7 @@ func (t *AppendFileTool) Parameters() map[string]any {
},
"content": map[string]any{
"type": "string",
"description": "The content to append. In `function.arguments`, use \\n for newline and \\\\n for literal backslash-n.",
"description": "The content to append. Standard JSON escaping applies: \\n for newline and \\\\n for literal backslash-n.",
},
},
"required": []string{"path", "content"},
+2 -2
View File
@@ -870,7 +870,7 @@ func (t *WriteFileTool) Name() string {
}
func (t *WriteFileTool) Description() string {
return "Write content to a file. In `function.arguments`, use \\n for a newline and \\\\n for a literal backslash-n sequence. Content is written byte-for-byte after argument decoding. If the file already exists, you must set overwrite=true to replace it."
return "Write content to a file. Content is written byte-for-byte after argument decoding. Standard JSON escaping applies: \\n for newline and \\\\n for a literal backslash-n sequence. If the file already exists, you must set overwrite=true to replace it."
}
func (t *WriteFileTool) Parameters() map[string]any {
@@ -883,7 +883,7 @@ func (t *WriteFileTool) Parameters() map[string]any {
},
"content": map[string]any{
"type": "string",
"description": "Content to write to the file. In `function.arguments`, use \\n for newline and \\\\n for literal backslash-n.",
"description": "Content to write to the file. Standard JSON escaping applies: \\n for newline and \\\\n for literal backslash-n.",
},
"overwrite": map[string]any{
"type": "boolean",
+33 -5
View File
@@ -3,14 +3,21 @@ package tools
import (
"context"
"fmt"
"sync/atomic"
"sync"
)
type SendCallbackWithContext func(ctx context.Context, channel, chatID, content, replyToMessageID string) error
// sentTarget records the channel+chatID that the message tool sent to.
type sentTarget struct {
Channel string
ChatID string
}
type MessageTool struct {
sendCallback SendCallbackWithContext
sentInRound atomic.Bool // Tracks whether a message was sent in the current processing round
mu sync.Mutex
sentTargets []sentTarget // Tracks all targets sent to in the current round
}
func NewMessageTool() *MessageTool {
@@ -53,12 +60,30 @@ func (t *MessageTool) Parameters() map[string]any {
// ResetSentInRound resets the per-round send tracker.
// Called by the agent loop at the start of each inbound message processing round.
func (t *MessageTool) ResetSentInRound() {
t.sentInRound.Store(false)
t.mu.Lock()
t.sentTargets = t.sentTargets[:0]
t.mu.Unlock()
}
// HasSentInRound returns true if the message tool sent a message during the current round.
func (t *MessageTool) HasSentInRound() bool {
return t.sentInRound.Load()
t.mu.Lock()
defer t.mu.Unlock()
return len(t.sentTargets) > 0
}
// HasSentTo returns true if the message tool sent to the specific channel+chatID
// during the current round. Used by PublishResponseIfNeeded to avoid suppressing
// the final response when the message tool only sent to a different conversation.
func (t *MessageTool) HasSentTo(channel, chatID string) bool {
t.mu.Lock()
defer t.mu.Unlock()
for _, st := range t.sentTargets {
if st.Channel == channel && st.ChatID == chatID {
return true
}
}
return false
}
func (t *MessageTool) SetSendCallback(callback SendCallbackWithContext) {
@@ -98,7 +123,10 @@ func (t *MessageTool) Execute(ctx context.Context, args map[string]any) *ToolRes
}
}
t.sentInRound.Store(true)
t.mu.Lock()
t.sentTargets = append(t.sentTargets, sentTarget{Channel: channel, ChatID: chatID})
t.mu.Unlock()
// Silent: user already received the message directly
return &ToolResult{
ForLLM: fmt.Sprintf("Message sent to %s:%s", channel, chatID),
+12 -7
View File
@@ -20,6 +20,7 @@ import (
"github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/constants"
"github.com/sipeed/picoclaw/pkg/isolation"
)
var (
@@ -120,7 +121,7 @@ func NewExecTool(workingDir string, restrict bool, allowPaths ...[]*regexp.Regex
func NewExecToolWithConfig(
workingDir string,
restrict bool,
config *config.Config,
cfg *config.Config,
allowPaths ...[]*regexp.Regexp,
) (*ExecTool, error) {
denyPatterns := make([]*regexp.Regexp, 0)
@@ -131,8 +132,8 @@ func NewExecToolWithConfig(
allowedPathPatterns = allowPaths[0]
}
if config != nil {
execConfig := config.Tools.Exec
if cfg != nil {
execConfig := cfg.Tools.Exec
enableDenyPatterns := execConfig.EnableDenyPatterns
allowRemote = execConfig.AllowRemote
if enableDenyPatterns {
@@ -163,8 +164,8 @@ func NewExecToolWithConfig(
}
var timeout time.Duration
if config != nil && config.Tools.Exec.TimeoutSeconds > 0 {
timeout = time.Duration(config.Tools.Exec.TimeoutSeconds) * time.Second
if cfg != nil && cfg.Tools.Exec.TimeoutSeconds > 0 {
timeout = time.Duration(cfg.Tools.Exec.TimeoutSeconds) * time.Second
}
return &ExecTool{
@@ -378,7 +379,9 @@ func (t *ExecTool) runSync(ctx context.Context, command, cwd string) *ToolResult
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Start(); err != nil {
// Route shell execution through the shared isolation entry point so exec tool
// subprocesses receive the same isolation policy as other integrations.
if err := isolation.Start(cmd); err != nil {
return ErrorResult(fmt.Sprintf("failed to start command: %v", err))
}
@@ -521,7 +524,9 @@ func (t *ExecTool) runBackground(ctx context.Context, command, cwd string, ptyEn
session.stdinWriter = stdinWriter
}
if err := cmd.Start(); err != nil {
// Background sessions use the same startup path so isolation stays consistent
// with synchronous exec runs.
if err := isolation.Start(cmd); err != nil {
if session.ptyMaster != nil {
session.ptyMaster.Close()
}