package tools import ( "context" "sync" "github.com/google/uuid" "github.com/nextlevelbuilder/goclaw/internal/config" "github.com/nextlevelbuilder/goclaw/internal/sandbox" ) // Tool execution context keys. // These replace mutable setter fields on tool instances, making tools thread-safe // for concurrent execution. Values are injected into context by the registry // and read by individual tools during Execute(). type toolContextKey string const ( ctxChannel toolContextKey = "tool_channel" ctxChannelType toolContextKey = "tool_channel_type" ctxChatID toolContextKey = "tool_chat_id" ctxPeerKind toolContextKey = "tool_peer_kind" ctxLocalKey toolContextKey = "tool_local_key" // composite key with topic/thread suffix for routing ctxSandboxKey toolContextKey = "tool_sandbox_key" ctxAsyncCB toolContextKey = "tool_async_cb" ctxWorkspace toolContextKey = "tool_workspace" ctxAgentKey toolContextKey = "tool_agent_key" ctxSessionKey toolContextKey = "tool_session_key" // origin session key for announce routing ) // Well-known channel names used for routing and access control. const ( ChannelSystem = "system" ChannelDashboard = "dashboard" ChannelTeammate = "teammate" ) // MediaPathLoader resolves a media ID to a local file path. // Used by media analysis tools (read_document, read_audio, read_video). type MediaPathLoader interface { LoadPath(id string) (string, error) } func WithToolChannel(ctx context.Context, channel string) context.Context { return context.WithValue(ctx, ctxChannel, channel) } func ToolChannelFromCtx(ctx context.Context) string { v, _ := ctx.Value(ctxChannel).(string) return v } func WithToolChannelType(ctx context.Context, channelType string) context.Context { return context.WithValue(ctx, ctxChannelType, channelType) } func ToolChannelTypeFromCtx(ctx context.Context) string { v, _ := ctx.Value(ctxChannelType).(string) return v } func WithToolChatID(ctx context.Context, chatID string) context.Context { return context.WithValue(ctx, ctxChatID, chatID) } func ToolChatIDFromCtx(ctx context.Context) string { v, _ := ctx.Value(ctxChatID).(string) return v } func WithToolPeerKind(ctx context.Context, peerKind string) context.Context { return context.WithValue(ctx, ctxPeerKind, peerKind) } func ToolPeerKindFromCtx(ctx context.Context) string { v, _ := ctx.Value(ctxPeerKind).(string) return v } // WithToolLocalKey injects the composite local key (e.g. "-100123:topic:42") into context. // Used by delegation/subagent to preserve topic routing info for announce-back. func WithToolLocalKey(ctx context.Context, localKey string) context.Context { return context.WithValue(ctx, ctxLocalKey, localKey) } func ToolLocalKeyFromCtx(ctx context.Context) string { v, _ := ctx.Value(ctxLocalKey).(string) return v } func WithToolSandboxKey(ctx context.Context, key string) context.Context { return context.WithValue(ctx, ctxSandboxKey, key) } func ToolSandboxKeyFromCtx(ctx context.Context) string { v, _ := ctx.Value(ctxSandboxKey).(string) return v } func WithToolAsyncCB(ctx context.Context, cb AsyncCallback) context.Context { return context.WithValue(ctx, ctxAsyncCB, cb) } func ToolAsyncCBFromCtx(ctx context.Context) AsyncCallback { v, _ := ctx.Value(ctxAsyncCB).(AsyncCallback) return v } func WithToolWorkspace(ctx context.Context, ws string) context.Context { return context.WithValue(ctx, ctxWorkspace, ws) } func ToolWorkspaceFromCtx(ctx context.Context) string { v, _ := ctx.Value(ctxWorkspace).(string) return v } // WithToolAgentKey injects the calling agent's key into context. // Multiple agents share a single tool registry; the agent key // lets tools like spawn/subagent identify which agent is the parent. func WithToolAgentKey(ctx context.Context, key string) context.Context { return context.WithValue(ctx, ctxAgentKey, key) } func ToolAgentKeyFromCtx(ctx context.Context) string { v, _ := ctx.Value(ctxAgentKey).(string) return v } // WithToolSessionKey injects the parent's session key so subagent announce // can route results back to the exact same session (required for WS where // session keys don't follow BuildScopedSessionKey format). func WithToolSessionKey(ctx context.Context, key string) context.Context { return context.WithValue(ctx, ctxSessionKey, key) } func ToolSessionKeyFromCtx(ctx context.Context) string { v, _ := ctx.Value(ctxSessionKey).(string) return v } // --- Builtin tool settings (global DB overrides) --- const ctxBuiltinToolSettings toolContextKey = "tool_builtin_settings" // BuiltinToolSettings maps tool name → settings JSON bytes. type BuiltinToolSettings map[string][]byte func WithBuiltinToolSettings(ctx context.Context, settings BuiltinToolSettings) context.Context { return context.WithValue(ctx, ctxBuiltinToolSettings, settings) } func BuiltinToolSettingsFromCtx(ctx context.Context) BuiltinToolSettings { v, _ := ctx.Value(ctxBuiltinToolSettings).(BuiltinToolSettings) return v } // --- Per-agent restrict_to_workspace override --- const ctxRestrictWs toolContextKey = "tool_restrict_to_workspace" // WithRestrictToWorkspace injects a per-agent restrict_to_workspace override into context. func WithRestrictToWorkspace(ctx context.Context, restrict bool) context.Context { return context.WithValue(ctx, ctxRestrictWs, restrict) } // RestrictFromCtx returns the per-agent restrict_to_workspace override. func RestrictFromCtx(ctx context.Context) (bool, bool) { v, ok := ctx.Value(ctxRestrictWs).(bool) return v, ok } func effectiveRestrict(ctx context.Context, toolDefault bool) bool { if v, ok := RestrictFromCtx(ctx); ok { return v } return toolDefault } // --- Parent agent model (for subagent inheritance) --- const ctxParentModel toolContextKey = "tool_parent_model" // WithParentModel sets the parent agent's model in context so subagents can inherit it. func WithParentModel(ctx context.Context, model string) context.Context { return context.WithValue(ctx, ctxParentModel, model) } // ParentModelFromCtx returns the parent agent's model from context. func ParentModelFromCtx(ctx context.Context) string { v, _ := ctx.Value(ctxParentModel).(string) return v } // --- Parent agent provider (for subagent inheritance) --- const ctxParentProvider toolContextKey = "tool_parent_provider" // WithParentProvider sets the parent agent's provider name in context so subagents inherit it. func WithParentProvider(ctx context.Context, providerName string) context.Context { return context.WithValue(ctx, ctxParentProvider, providerName) } // ParentProviderFromCtx returns the parent agent's provider name from context. func ParentProviderFromCtx(ctx context.Context) string { v, _ := ctx.Value(ctxParentProvider).(string) return v } // --- Per-agent subagent config override --- const ctxSubagentCfg toolContextKey = "tool_subagent_config" func WithSubagentConfig(ctx context.Context, cfg *config.SubagentsConfig) context.Context { return context.WithValue(ctx, ctxSubagentCfg, cfg) } func SubagentConfigFromCtx(ctx context.Context) *config.SubagentsConfig { v, _ := ctx.Value(ctxSubagentCfg).(*config.SubagentsConfig) return v } // --- Per-agent memory config override --- const ctxMemoryCfg toolContextKey = "tool_memory_config" func WithMemoryConfig(ctx context.Context, cfg *config.MemoryConfig) context.Context { return context.WithValue(ctx, ctxMemoryCfg, cfg) } func MemoryConfigFromCtx(ctx context.Context) *config.MemoryConfig { v, _ := ctx.Value(ctxMemoryCfg).(*config.MemoryConfig) return v } // --- Team ID propagation (task dispatch → workspace tools) --- const ctxTeamID toolContextKey = "tool_team_id" // WithToolTeamID injects the dispatching team's ID into context so team // tools (team_tasks, team_message) and the WorkspaceInterceptor resolve // the correct team when the agent belongs to multiple teams. func WithToolTeamID(ctx context.Context, teamID string) context.Context { return context.WithValue(ctx, ctxTeamID, teamID) } // ToolTeamIDFromCtx returns the dispatching team's ID from context. func ToolTeamIDFromCtx(ctx context.Context) string { v, _ := ctx.Value(ctxTeamID).(string) return v } // --- Team workspace path (accessible but not default) --- const ctxTeamWorkspace toolContextKey = "tool_team_workspace" // WithToolTeamWorkspace stores the team shared workspace directory path. // File tools allow access to this path even when restrict_to_workspace is true. func WithToolTeamWorkspace(ctx context.Context, dir string) context.Context { return context.WithValue(ctx, ctxTeamWorkspace, dir) } // ToolTeamWorkspaceFromCtx returns the team shared workspace directory path. func ToolTeamWorkspaceFromCtx(ctx context.Context) string { v, _ := ctx.Value(ctxTeamWorkspace).(string) return v } // --- Team task ID propagation (delegation origin → workspace tools) --- const ctxTeamTaskID toolContextKey = "tool_team_task_id" // WithTeamTaskID injects the delegation's team task ID into context // so workspace tools can auto-link files to the active task. func WithTeamTaskID(ctx context.Context, taskID string) context.Context { return context.WithValue(ctx, ctxTeamTaskID, taskID) } // TeamTaskIDFromCtx returns the delegation's team task ID from context. func TeamTaskIDFromCtx(ctx context.Context) string { v, _ := ctx.Value(ctxTeamTaskID).(string) return v } // --- Workspace scope propagation (delegation origin) --- const ( ctxWsChannel toolContextKey = "tool_workspace_channel" ctxWsChatID toolContextKey = "tool_workspace_chat_id" ) func WithWorkspaceChannel(ctx context.Context, channel string) context.Context { return context.WithValue(ctx, ctxWsChannel, channel) } func WorkspaceChannelFromCtx(ctx context.Context) string { v, _ := ctx.Value(ctxWsChannel).(string) return v } func WithWorkspaceChatID(ctx context.Context, chatID string) context.Context { return context.WithValue(ctx, ctxWsChatID, chatID) } func WorkspaceChatIDFromCtx(ctx context.Context) string { v, _ := ctx.Value(ctxWsChatID).(string) return v } // --- Pending team task dispatch (post-turn processing) --- const ctxPendingDispatch toolContextKey = "tool_pending_team_dispatch" // PendingTeamDispatch tracks team tasks created during an agent turn. // After the turn ends, the consumer drains and dispatches them. // Thread-safe: tools may execute in parallel goroutines. type PendingTeamDispatch struct { mu sync.Mutex tasks map[uuid.UUID][]uuid.UUID // teamID → []taskID listed bool // true after list called in this turn teamLock *sync.Mutex // acquired on list, released before post-turn dispatch } func NewPendingTeamDispatch() *PendingTeamDispatch { return &PendingTeamDispatch{tasks: make(map[uuid.UUID][]uuid.UUID)} } // Add records a task created during this turn. func (p *PendingTeamDispatch) Add(teamID, taskID uuid.UUID) { p.mu.Lock() p.tasks[teamID] = append(p.tasks[teamID], taskID) p.mu.Unlock() } // Drain returns all tracked tasks and resets the container. func (p *PendingTeamDispatch) Drain() map[uuid.UUID][]uuid.UUID { p.mu.Lock() defer p.mu.Unlock() out := p.tasks p.tasks = make(map[uuid.UUID][]uuid.UUID) return out } // MarkListed records that list was called in this turn. func (p *PendingTeamDispatch) MarkListed() { p.mu.Lock() p.listed = true p.mu.Unlock() } // HasListed reports whether list was called in this turn. func (p *PendingTeamDispatch) HasListed() bool { p.mu.Lock() defer p.mu.Unlock() return p.listed } // SetTeamLock stores the acquired team create lock so it can be released post-turn. func (p *PendingTeamDispatch) SetTeamLock(m *sync.Mutex) { p.mu.Lock() p.teamLock = m p.mu.Unlock() } // ReleaseTeamLock releases the held team create lock, if any. func (p *PendingTeamDispatch) ReleaseTeamLock() { p.mu.Lock() if p.teamLock != nil { p.teamLock.Unlock() p.teamLock = nil } p.mu.Unlock() } func WithPendingTeamDispatch(ctx context.Context, ptd *PendingTeamDispatch) context.Context { return context.WithValue(ctx, ctxPendingDispatch, ptd) } func PendingTeamDispatchFromCtx(ctx context.Context) *PendingTeamDispatch { v, _ := ctx.Value(ctxPendingDispatch).(*PendingTeamDispatch) return v } // --- Run media file paths (for team workspace auto-collect) --- const ctxRunMediaPaths toolContextKey = "tool_run_media_paths" // WithRunMediaPaths stores the absolute file paths of media files received // in the current run. Used by team_tasks to auto-copy files to team workspace. func WithRunMediaPaths(ctx context.Context, paths []string) context.Context { return context.WithValue(ctx, ctxRunMediaPaths, paths) } // RunMediaPathsFromCtx returns media file paths from the current run. func RunMediaPathsFromCtx(ctx context.Context) []string { v, _ := ctx.Value(ctxRunMediaPaths).([]string) return v } const ctxRunMediaNames toolContextKey = "tool_run_media_names" // WithRunMediaNames stores the mapping from media file path to original filename. func WithRunMediaNames(ctx context.Context, names map[string]string) context.Context { return context.WithValue(ctx, ctxRunMediaNames, names) } // RunMediaNamesFromCtx returns the media path → original filename mapping. func RunMediaNamesFromCtx(ctx context.Context) map[string]string { v, _ := ctx.Value(ctxRunMediaNames).(map[string]string) return v } // --- Per-agent sandbox config override --- const ctxSandboxCfg toolContextKey = "tool_sandbox_config" func WithSandboxConfig(ctx context.Context, cfg *sandbox.Config) context.Context { return context.WithValue(ctx, ctxSandboxCfg, cfg) } func SandboxConfigFromCtx(ctx context.Context) *sandbox.Config { v, _ := ctx.Value(ctxSandboxCfg).(*sandbox.Config) return v }