mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
merge: integrate main into refactor-inbound-context-routing-session
This commit is contained in:
+5
-5
@@ -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"},
|
||||
|
||||
@@ -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
@@ -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
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user