feat: Add Human-in-the-Loop tool execution approval
- Update `Tool` interface with `RequiresApproval() bool` - Make `ExecTool` require explicit user approval - Implement `pendingApprovals` state management in `AgentLoop` - Pause execution in `runLLMIteration` to prompt user for "Yes/No" - Refactor `loop_process.go` and `loop_llm.go` to inject user approval/rejection feedback to LLM context before resuming Co-authored-by: hobbyistlabs-coder <267281733+hobbyistlabs-coder@users.noreply.github.com>
This commit is contained in:
parent
e99a5ae7f3
commit
1fcc9137a8
30 changed files with 305 additions and 15 deletions
|
|
@ -16,7 +16,7 @@ func TestNewAgentCommand(t *testing.T) {
|
||||||
assert.Equal(t, "Interact with the agent directly", cmd.Short)
|
assert.Equal(t, "Interact with the agent directly", cmd.Short)
|
||||||
|
|
||||||
assert.Len(t, cmd.Aliases, 0)
|
assert.Len(t, cmd.Aliases, 0)
|
||||||
assert.False(t, cmd.HasSubCommands())
|
assert.True(t, cmd.HasSubCommands())
|
||||||
|
|
||||||
assert.Nil(t, cmd.Run)
|
assert.Nil(t, cmd.Run)
|
||||||
assert.NotNil(t, cmd.RunE)
|
assert.NotNil(t, cmd.RunE)
|
||||||
|
|
|
||||||
|
|
@ -21,20 +21,31 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type AgentLoop struct {
|
type AgentLoop struct {
|
||||||
bus *bus.MessageBus
|
bus *bus.MessageBus
|
||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
registry *AgentRegistry
|
registry *AgentRegistry
|
||||||
state *state.Manager
|
state *state.Manager
|
||||||
running atomic.Bool
|
running atomic.Bool
|
||||||
summarizing sync.Map
|
summarizing sync.Map
|
||||||
summaryJobs chan summaryJob
|
pendingApprovals sync.Map // Tracks state for Human-in-the-Loop approvals
|
||||||
wg sync.WaitGroup
|
summaryJobs chan summaryJob
|
||||||
fallback *providers.FallbackChain
|
wg sync.WaitGroup
|
||||||
channelManager *channels.Manager
|
fallback *providers.FallbackChain
|
||||||
mediaStore media.MediaStore
|
channelManager *channels.Manager
|
||||||
transcriber voice.Transcriber
|
mediaStore media.MediaStore
|
||||||
cmdRegistry *commands.Registry
|
transcriber voice.Transcriber
|
||||||
mcp mcpRuntime
|
cmdRegistry *commands.Registry
|
||||||
|
mcp mcpRuntime
|
||||||
|
}
|
||||||
|
|
||||||
|
type pendingApprovalState struct {
|
||||||
|
agent *AgentInstance
|
||||||
|
opts processOptions
|
||||||
|
normalizedToolCalls []providers.ToolCall
|
||||||
|
messages []providers.Message
|
||||||
|
iteration int
|
||||||
|
activeCandidates []providers.FallbackCandidate
|
||||||
|
activeModel string
|
||||||
}
|
}
|
||||||
|
|
||||||
// processOptions configures how a message is processed
|
// processOptions configures how a message is processed
|
||||||
|
|
|
||||||
|
|
@ -315,6 +315,49 @@ func (al *AgentLoop) runLLMIteration(
|
||||||
// Save assistant message with tool calls to session
|
// Save assistant message with tool calls to session
|
||||||
agent.Sessions.AddFullMessage(opts.SessionKey, assistantMsg)
|
agent.Sessions.AddFullMessage(opts.SessionKey, assistantMsg)
|
||||||
|
|
||||||
|
// --- HITL: Check if any tool requires human approval ---
|
||||||
|
requiresApproval := false
|
||||||
|
for _, tc := range normalizedToolCalls {
|
||||||
|
if t, ok := agent.Tools.Get(tc.Name); ok && t.RequiresApproval() {
|
||||||
|
requiresApproval = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if requiresApproval {
|
||||||
|
logger.InfoCF("agent", "Tool execution paused for user approval", map[string]any{
|
||||||
|
"agent_id": agent.ID,
|
||||||
|
"session_key": opts.SessionKey,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Format approval message
|
||||||
|
approvalMsg := "The following tool execution requires your approval:\n"
|
||||||
|
for _, tc := range normalizedToolCalls {
|
||||||
|
argsJSON, _ := json.MarshalIndent(tc.Arguments, "", " ")
|
||||||
|
approvalMsg += fmt.Sprintf("\n- `%s`:\n```json\n%s\n```\n", tc.Name, string(argsJSON))
|
||||||
|
}
|
||||||
|
approvalMsg += "\nDo you approve? (Yes/No)"
|
||||||
|
|
||||||
|
al.pendingApprovals.Store(opts.SessionKey, pendingApprovalState{
|
||||||
|
agent: agent,
|
||||||
|
opts: opts,
|
||||||
|
normalizedToolCalls: normalizedToolCalls,
|
||||||
|
messages: messages,
|
||||||
|
iteration: iteration,
|
||||||
|
activeCandidates: activeCandidates,
|
||||||
|
activeModel: activeModel,
|
||||||
|
})
|
||||||
|
|
||||||
|
al.bus.PublishOutbound(ctx, bus.OutboundMessage{
|
||||||
|
Channel: opts.Channel,
|
||||||
|
ChatID: opts.ChatID,
|
||||||
|
Content: approvalMsg,
|
||||||
|
})
|
||||||
|
|
||||||
|
return "", iteration, nil
|
||||||
|
}
|
||||||
|
// --- End HITL ---
|
||||||
|
|
||||||
// Execute tool calls in parallel
|
// Execute tool calls in parallel
|
||||||
agentResults := al.executeToolBatch(ctx, agent, opts, normalizedToolCalls, iteration)
|
agentResults := al.executeToolBatch(ctx, agent, opts, normalizedToolCalls, iteration)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"jane/pkg/bus"
|
"jane/pkg/bus"
|
||||||
"jane/pkg/constants"
|
"jane/pkg/constants"
|
||||||
"jane/pkg/logger"
|
"jane/pkg/logger"
|
||||||
|
"jane/pkg/providers"
|
||||||
"jane/pkg/routing"
|
"jane/pkg/routing"
|
||||||
"jane/pkg/utils"
|
"jane/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
@ -148,6 +149,126 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage)
|
||||||
return response, nil
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HITL: Check for pending approvals
|
||||||
|
if val, ok := al.pendingApprovals.Load(sessionKey); ok {
|
||||||
|
pending := val.(pendingApprovalState)
|
||||||
|
|
||||||
|
responseStr := strings.ToLower(strings.TrimSpace(msg.Content))
|
||||||
|
isYes := responseStr == "yes" || responseStr == "y"
|
||||||
|
isNo := responseStr == "no" || responseStr == "n"
|
||||||
|
|
||||||
|
if isYes || isNo {
|
||||||
|
al.pendingApprovals.Delete(sessionKey)
|
||||||
|
|
||||||
|
if isNo {
|
||||||
|
logger.InfoCF("agent", "User rejected tool execution", map[string]any{
|
||||||
|
"agent_id": agent.ID,
|
||||||
|
"session_key": sessionKey,
|
||||||
|
})
|
||||||
|
for _, tc := range pending.normalizedToolCalls {
|
||||||
|
rejectMsg := providers.Message{
|
||||||
|
Role: "tool",
|
||||||
|
Content: "User rejected tool execution",
|
||||||
|
ToolCallID: tc.ID,
|
||||||
|
}
|
||||||
|
pending.messages = append(pending.messages, rejectMsg)
|
||||||
|
agent.Sessions.AddFullMessage(sessionKey, rejectMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tick TTL since we bypass normal execution where it happens
|
||||||
|
agent.Tools.TickTTL()
|
||||||
|
|
||||||
|
// Continue loop with rejection feedback
|
||||||
|
finalContent, _, err := al.runLLMIteration(ctx, pending.agent, pending.messages, pending.opts)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update session and return
|
||||||
|
if finalContent == "" {
|
||||||
|
finalContent = pending.opts.DefaultResponse
|
||||||
|
}
|
||||||
|
agent.Sessions.AddMessage(sessionKey, "assistant", finalContent)
|
||||||
|
agent.Sessions.Save(sessionKey)
|
||||||
|
return finalContent, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if isYes {
|
||||||
|
logger.InfoCF("agent", "User approved tool execution", map[string]any{
|
||||||
|
"agent_id": agent.ID,
|
||||||
|
"session_key": sessionKey,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Execute the approved tools
|
||||||
|
agentResults := al.executeToolBatch(ctx, pending.agent, pending.opts, pending.normalizedToolCalls, pending.iteration)
|
||||||
|
|
||||||
|
// Inject results into context, matching original logic from loop_llm.go
|
||||||
|
for _, r := range agentResults {
|
||||||
|
if !r.result.Silent && r.result.ForUser != "" && pending.opts.SendResponse {
|
||||||
|
al.bus.PublishOutbound(ctx, bus.OutboundMessage{
|
||||||
|
Channel: pending.opts.Channel,
|
||||||
|
ChatID: pending.opts.ChatID,
|
||||||
|
Content: r.result.ForUser,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(r.result.Media) > 0 {
|
||||||
|
parts := make([]bus.MediaPart, 0, len(r.result.Media))
|
||||||
|
for _, ref := range r.result.Media {
|
||||||
|
part := bus.MediaPart{Ref: ref}
|
||||||
|
if al.mediaStore != nil {
|
||||||
|
if _, meta, err := al.mediaStore.ResolveWithMeta(ref); err == nil {
|
||||||
|
part.Filename = meta.Filename
|
||||||
|
part.ContentType = meta.ContentType
|
||||||
|
part.Type = inferMediaType(meta.Filename, meta.ContentType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parts = append(parts, part)
|
||||||
|
}
|
||||||
|
al.bus.PublishOutboundMedia(ctx, bus.OutboundMediaMessage{
|
||||||
|
Channel: pending.opts.Channel,
|
||||||
|
ChatID: pending.opts.ChatID,
|
||||||
|
Parts: parts,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
contentForLLM := r.result.ForLLM
|
||||||
|
if contentForLLM == "" && r.result.Err != nil {
|
||||||
|
contentForLLM = r.result.Err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
toolResultMsg := providers.Message{
|
||||||
|
Role: "tool",
|
||||||
|
Content: contentForLLM,
|
||||||
|
ToolCallID: r.tc.ID,
|
||||||
|
}
|
||||||
|
pending.messages = append(pending.messages, toolResultMsg)
|
||||||
|
agent.Sessions.AddFullMessage(sessionKey, toolResultMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
agent.Tools.TickTTL()
|
||||||
|
|
||||||
|
// Continue loop with execution feedback
|
||||||
|
finalContent, _, err := al.runLLMIteration(ctx, pending.agent, pending.messages, pending.opts)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update session and return
|
||||||
|
if finalContent == "" {
|
||||||
|
finalContent = pending.opts.DefaultResponse
|
||||||
|
}
|
||||||
|
agent.Sessions.AddMessage(sessionKey, "assistant", finalContent)
|
||||||
|
agent.Sessions.Save(sessionKey)
|
||||||
|
return finalContent, nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Ask again
|
||||||
|
return "Please respond with Yes or No to approve the tool execution.", nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// End HITL
|
||||||
|
|
||||||
return al.runAgentLoop(ctx, agent, opts)
|
return al.runAgentLoop(ctx, agent, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1160,3 +1160,5 @@ func TestResolveMediaRefs_UsesMetaContentType(t *testing.T) {
|
||||||
t.Fatalf("expected jpeg prefix, got %q", result[0].Media[0][:30])
|
t.Fatalf("expected jpeg prefix, got %q", result[0].Media[0][:30])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *mockCustomTool) RequiresApproval() bool { return false }
|
||||||
|
|
|
||||||
|
|
@ -127,3 +127,7 @@ func (t *AlpacaTool) getSMA(symbol string) *tools.ToolResult {
|
||||||
func init() {
|
func init() {
|
||||||
// tools.Register(&AlpacaTool{}) // We will register it manually where we have access to config.
|
// tools.Register(&AlpacaTool{}) // We will register it manually where we have access to config.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *AlpacaTool) RequiresApproval() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ type Tool interface {
|
||||||
Description() string
|
Description() string
|
||||||
Parameters() map[string]any
|
Parameters() map[string]any
|
||||||
Execute(ctx context.Context, args map[string]any) *ToolResult
|
Execute(ctx context.Context, args map[string]any) *ToolResult
|
||||||
|
RequiresApproval() bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Request-scoped tool context (channel / chatID) ---
|
// --- Request-scoped tool context (channel / chatID) ---
|
||||||
|
|
|
||||||
|
|
@ -250,3 +250,7 @@ func (t *BrowserActionTool) Close() {
|
||||||
t.pw = nil
|
t.pw = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *BrowserActionTool) RequiresApproval() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,3 +56,7 @@ func (t *CalculatorTool) Execute(ctx context.Context, args map[string]any) *Tool
|
||||||
resStr := fmt.Sprintf("%v", result)
|
resStr := fmt.Sprintf("%v", result)
|
||||||
return UserResult(resStr)
|
return UserResult(resStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *CalculatorTool) RequiresApproval() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -345,3 +345,7 @@ func (t *CronTool) ExecuteJob(ctx context.Context, job *cron.CronJob) string {
|
||||||
_ = response // Will be sent by AgentLoop
|
_ = response // Will be sent by AgentLoop
|
||||||
return "ok"
|
return "ok"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *CronTool) RequiresApproval() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -172,3 +172,11 @@ func replaceEditContent(content []byte, oldText, newText string) ([]byte, error)
|
||||||
newContent := strings.Replace(contentStr, oldText, newText, 1)
|
newContent := strings.Replace(contentStr, oldText, newText, 1)
|
||||||
return []byte(newContent), nil
|
return []byte(newContent), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *EditFileTool) RequiresApproval() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *AppendFileTool) RequiresApproval() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -696,3 +696,15 @@ func getSafeRelPath(workspace, path string) (string, error) {
|
||||||
|
|
||||||
return rel, nil
|
return rel, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *ReadFileTool) RequiresApproval() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *WriteFileTool) RequiresApproval() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ListDirTool) RequiresApproval() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -145,3 +145,7 @@ func (t *GoEvalTool) Execute(ctx context.Context, args map[string]any) *ToolResu
|
||||||
ForUser: output,
|
ForUser: output,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *GoEvalTool) RequiresApproval() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -155,3 +155,7 @@ func parseI2CBus(args map[string]any) (string, *ToolResult) {
|
||||||
}
|
}
|
||||||
return bus, nil
|
return bus, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *I2CTool) RequiresApproval() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -282,3 +282,7 @@ func splitQuoted(s string) []string {
|
||||||
}
|
}
|
||||||
return parts
|
return parts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *MCP2CliTool) RequiresApproval() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -244,3 +244,7 @@ func extractContentText(content []mcp.Content) string {
|
||||||
}
|
}
|
||||||
return strings.Join(parts, "\n")
|
return strings.Join(parts, "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *MCPTool) RequiresApproval() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -100,3 +100,7 @@ func (t *MessageTool) Execute(ctx context.Context, args map[string]any) *ToolRes
|
||||||
Silent: true,
|
Silent: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *MessageTool) RequiresApproval() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -329,3 +329,7 @@ func (r *ToolRegistry) GetSummaries() []string {
|
||||||
}
|
}
|
||||||
return summaries
|
return summaries
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *ToolRegistry) RequiresApproval() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -358,3 +358,7 @@ func TestToolRegistry_ConcurrentAccess(t *testing.T) {
|
||||||
t.Error("expected tools to be registered after concurrent access")
|
t.Error("expected tools to be registered after concurrent access")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *mockRegistryTool) RequiresApproval() bool { return false }
|
||||||
|
func (t *mockContextAwareTool) RequiresApproval() bool { return false }
|
||||||
|
func (t *mockAsyncRegistryTool) RequiresApproval() bool { return false }
|
||||||
|
|
|
||||||
|
|
@ -302,3 +302,11 @@ func (r *ToolRegistry) SearchBM25(query string, maxSearchResults int) []ToolSear
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *RegexSearchTool) RequiresApproval() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *BM25SearchTool) RequiresApproval() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -337,3 +337,5 @@ func TestPromoteTools_ConcurrentWithTickTTL(t *testing.T) {
|
||||||
}
|
}
|
||||||
<-done
|
<-done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *mockSearchableTool) RequiresApproval() bool { return false }
|
||||||
|
|
|
||||||
|
|
@ -148,3 +148,7 @@ func detectMediaType(path string) string {
|
||||||
|
|
||||||
return "application/octet-stream"
|
return "application/octet-stream"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *SendFileTool) RequiresApproval() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -418,3 +418,7 @@ func (t *ExecTool) SetAllowPatterns(patterns []string) error {
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *ExecTool) RequiresApproval() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -201,3 +201,7 @@ func writeOriginMeta(targetDir, registryName, slug, version string) error {
|
||||||
// Use unified atomic write utility with explicit sync for flash storage reliability.
|
// Use unified atomic write utility with explicit sync for flash storage reliability.
|
||||||
return fileutil.WriteFileAtomic(filepath.Join(targetDir, ".skill-origin.json"), data, 0o600)
|
return fileutil.WriteFileAtomic(filepath.Join(targetDir, ".skill-origin.json"), data, 0o600)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *InstallSkillTool) RequiresApproval() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -117,3 +117,7 @@ func formatSearchResults(query string, results []skills.SearchResult, cached boo
|
||||||
sb.WriteString("Use install_skill with the slug to install a skill.")
|
sb.WriteString("Use install_skill with the slug to install a skill.")
|
||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *FindSkillsTool) RequiresApproval() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -104,3 +104,7 @@ func (t *SpawnTool) execute(ctx context.Context, args map[string]any, cb AsyncCa
|
||||||
// Return AsyncResult since the task runs in background
|
// Return AsyncResult since the task runs in background
|
||||||
return AsyncResult(result)
|
return AsyncResult(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *SpawnTool) RequiresApproval() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -160,3 +160,7 @@ func parseSPIArgs(args map[string]any) (device string, speed uint32, mode uint8,
|
||||||
|
|
||||||
return dev, speed, mode, bits, ""
|
return dev, speed, mode, bits, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *SPITool) RequiresApproval() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -359,3 +359,7 @@ func (t *SubagentTool) Execute(ctx context.Context, args map[string]any) *ToolRe
|
||||||
Async: false,
|
Async: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *SubagentTool) RequiresApproval() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
||||||
5
pkg/tools/web/fetch_tool.go
Normal file
5
pkg/tools/web/fetch_tool.go
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
package web
|
||||||
|
|
||||||
|
func (t *WebFetchTool) RequiresApproval() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
5
pkg/tools/web/search_tool.go
Normal file
5
pkg/tools/web/search_tool.go
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
package web
|
||||||
|
|
||||||
|
func (t *WebSearchTool) RequiresApproval() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue