mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
aef1e8e8c4
* fix: eliminate data races on shared tool instances Signed-off-by: Boris Bliznioukov <blib@mail.com> * fix: remove unused indirect dependency on github.com/gdamore/tcell/v2 Signed-off-by: Boris Bliznioukov <blib@mail.com> * fix: reviewer comments improve context handling for tool execution and ensure defaults for non-conversation callers Signed-off-by: Boris Bliznioukov <blib@mail.com> --------- Signed-off-by: Boris Bliznioukov <blib@mail.com>
94 lines
3.1 KiB
Go
94 lines
3.1 KiB
Go
package tools
|
|
|
|
import "context"
|
|
|
|
// Tool is the interface that all tools must implement.
|
|
type Tool interface {
|
|
Name() string
|
|
Description() string
|
|
Parameters() map[string]any
|
|
Execute(ctx context.Context, args map[string]any) *ToolResult
|
|
}
|
|
|
|
// --- Request-scoped tool context (channel / chatID) ---
|
|
//
|
|
// Carried via context.Value so that concurrent tool calls each receive
|
|
// their own immutable copy — no mutable state on singleton tool instances.
|
|
//
|
|
// Keys are unexported pointer-typed vars — guaranteed collision-free,
|
|
// and only accessible through the helper functions below.
|
|
|
|
type toolCtxKey struct{ name string }
|
|
|
|
var (
|
|
ctxKeyChannel = &toolCtxKey{"channel"}
|
|
ctxKeyChatID = &toolCtxKey{"chatID"}
|
|
)
|
|
|
|
// WithToolContext returns a child context carrying channel and chatID.
|
|
func WithToolContext(ctx context.Context, channel, chatID string) context.Context {
|
|
ctx = context.WithValue(ctx, ctxKeyChannel, channel)
|
|
ctx = context.WithValue(ctx, ctxKeyChatID, chatID)
|
|
return ctx
|
|
}
|
|
|
|
// ToolChannel extracts the channel from ctx, or "" if unset.
|
|
func ToolChannel(ctx context.Context) string {
|
|
v, _ := ctx.Value(ctxKeyChannel).(string)
|
|
return v
|
|
}
|
|
|
|
// ToolChatID extracts the chatID from ctx, or "" if unset.
|
|
func ToolChatID(ctx context.Context) string {
|
|
v, _ := ctx.Value(ctxKeyChatID).(string)
|
|
return v
|
|
}
|
|
|
|
// AsyncCallback is a function type that async tools use to notify completion.
|
|
// When an async tool finishes its work, it calls this callback with the result.
|
|
//
|
|
// The ctx parameter allows the callback to be canceled if the agent is shutting down.
|
|
// The result parameter contains the tool's execution result.
|
|
type AsyncCallback func(ctx context.Context, result *ToolResult)
|
|
|
|
// AsyncExecutor is an optional interface that tools can implement to support
|
|
// asynchronous execution with completion callbacks.
|
|
//
|
|
// Unlike the old AsyncTool pattern (SetCallback + Execute), AsyncExecutor
|
|
// receives the callback as a parameter of ExecuteAsync. This eliminates the
|
|
// data race where concurrent calls could overwrite each other's callbacks
|
|
// on a shared tool instance.
|
|
//
|
|
// This is useful for:
|
|
// - Long-running operations that shouldn't block the agent loop
|
|
// - Subagent spawns that complete independently
|
|
// - Background tasks that need to report results later
|
|
//
|
|
// Example:
|
|
//
|
|
// func (t *SpawnTool) ExecuteAsync(ctx context.Context, args map[string]any, cb AsyncCallback) *ToolResult {
|
|
// go func() {
|
|
// result := t.runSubagent(ctx, args)
|
|
// if cb != nil { cb(ctx, result) }
|
|
// }()
|
|
// return AsyncResult("Subagent spawned, will report back")
|
|
// }
|
|
type AsyncExecutor interface {
|
|
Tool
|
|
// ExecuteAsync runs the tool asynchronously. The callback cb will be
|
|
// invoked (possibly from another goroutine) when the async operation
|
|
// completes. cb is guaranteed to be non-nil by the caller (registry).
|
|
ExecuteAsync(ctx context.Context, args map[string]any, cb AsyncCallback) *ToolResult
|
|
}
|
|
|
|
func ToolToSchema(tool Tool) map[string]any {
|
|
return map[string]any{
|
|
"type": "function",
|
|
"function": map[string]any{
|
|
"name": tool.Name(),
|
|
"description": tool.Description(),
|
|
"parameters": tool.Parameters(),
|
|
},
|
|
}
|
|
}
|