diff --git a/internal/agent/loop_context.go b/internal/agent/loop_context.go index a4ae77d3..fab68581 100644 --- a/internal/agent/loop_context.go +++ b/internal/agent/loop_context.go @@ -241,6 +241,42 @@ func (l *Loop) injectContext(ctx context.Context, req *RunRequest) (contextSetup ) } + // Build RunContext from all resolved values and inject as single context key. + // This provides a typed, inspectable snapshot of all loop-injected context. + // Individual With* keys above remain for backward compat during transition. + providerName := "" + if l.provider != nil { + providerName = l.provider.Name() + } + rc := &store.RunContext{ + AgentID: l.agentUUID, + AgentKey: l.id, + TenantID: l.tenantID, + UserID: req.UserID, + AgentType: l.agentType, + SenderID: req.SenderID, + SelfEvolve: l.selfEvolve, + SharedMemory: store.IsSharedMemory(ctx), + SharedKG: store.IsSharedKG(ctx), + RestrictToWorkspace: l.restrictToWs != nil && *l.restrictToWs, + BuiltinToolSettings: l.builtinToolSettings, + ChannelType: req.ChannelType, + SubagentsCfg: l.subagentsCfg, + ParentModel: l.model, + ParentProvider: providerName, + MemoryCfg: l.memoryCfg, + SandboxCfg: l.sandboxCfg, + ShellDenyGroups: l.shellDenyGroups, + Workspace: tools.ToolWorkspaceFromCtx(ctx), + TeamWorkspace: tools.ToolTeamWorkspaceFromCtx(ctx), + TeamID: tools.ToolTeamIDFromCtx(ctx), + WorkspaceChannel: req.WorkspaceChannel, + WorkspaceChatID: req.WorkspaceChatID, + TeamTaskID: req.TeamTaskID, + AgentToolKey: l.id, + } + ctx = store.WithRunContext(ctx, rc) + return contextSetupResult{ ctx: ctx, resolvedTeamSettings: resolvedTeamSettings, diff --git a/internal/store/context.go b/internal/store/context.go index a8a174d2..79860ccf 100644 --- a/internal/store/context.go +++ b/internal/store/context.go @@ -48,8 +48,13 @@ func WithShellDenyGroups(ctx context.Context, groups map[string]bool) context.Co // ShellDenyGroupsFromContext returns shell deny group overrides from the context, or nil. func ShellDenyGroupsFromContext(ctx context.Context) map[string]bool { - v, _ := ctx.Value(ShellDenyGroupsKey).(map[string]bool) - return v + if v, _ := ctx.Value(ShellDenyGroupsKey).(map[string]bool); v != nil { + return v + } + if rc := RunContextFromCtx(ctx); rc != nil { + return rc.ShellDenyGroups + } + return nil } // WithUserID returns a new context with the given user ID. @@ -59,9 +64,12 @@ func WithUserID(ctx context.Context, id string) context.Context { // UserIDFromContext extracts the user ID from context. Returns "" if not set. func UserIDFromContext(ctx context.Context) string { - if v, ok := ctx.Value(UserIDKey).(string); ok { + if v, ok := ctx.Value(UserIDKey).(string); ok && v != "" { return v } + if rc := RunContextFromCtx(ctx); rc != nil { + return rc.UserID + } return "" } @@ -72,9 +80,12 @@ func WithAgentID(ctx context.Context, id uuid.UUID) context.Context { // AgentIDFromContext extracts the agent UUID from context. Returns uuid.Nil if not set. func AgentIDFromContext(ctx context.Context) uuid.UUID { - if v, ok := ctx.Value(AgentIDKey).(uuid.UUID); ok { + if v, ok := ctx.Value(AgentIDKey).(uuid.UUID); ok && v != uuid.Nil { return v } + if rc := RunContextFromCtx(ctx); rc != nil { + return rc.AgentID + } return uuid.Nil } @@ -85,9 +96,12 @@ func WithAgentType(ctx context.Context, t string) context.Context { // AgentTypeFromContext extracts the agent type from context. Returns "" if not set. func AgentTypeFromContext(ctx context.Context) string { - if v, ok := ctx.Value(AgentTypeKey).(string); ok { + if v, ok := ctx.Value(AgentTypeKey).(string); ok && v != "" { return v } + if rc := RunContextFromCtx(ctx); rc != nil { + return rc.AgentType + } return "" } @@ -98,9 +112,12 @@ func WithAgentKey(ctx context.Context, key string) context.Context { // AgentKeyFromContext extracts the agent key from context. Returns "" if not set. func AgentKeyFromContext(ctx context.Context) string { - if v, ok := ctx.Value(AgentKeyKey).(string); ok { + if v, ok := ctx.Value(AgentKeyKey).(string); ok && v != "" { return v } + if rc := RunContextFromCtx(ctx); rc != nil { + return rc.AgentKey + } return "" } @@ -111,9 +128,12 @@ func WithSenderID(ctx context.Context, id string) context.Context { // SenderIDFromContext extracts the sender ID from context. Returns "" if not set. func SenderIDFromContext(ctx context.Context) string { - if v, ok := ctx.Value(SenderIDKey).(string); ok { + if v, ok := ctx.Value(SenderIDKey).(string); ok && v != "" { return v } + if rc := RunContextFromCtx(ctx); rc != nil { + return rc.SenderID + } return "" } @@ -124,8 +144,11 @@ func WithSelfEvolve(ctx context.Context, v bool) context.Context { // SelfEvolveFromContext extracts the self-evolve flag from context. Returns false if not set. func SelfEvolveFromContext(ctx context.Context) bool { - if v, ok := ctx.Value(SelfEvolveKey).(bool); ok { - return v + if v, ok := ctx.Value(SelfEvolveKey).(bool); ok && v { + return true + } + if rc := RunContextFromCtx(ctx); rc != nil { + return rc.SelfEvolve } return false } @@ -137,8 +160,13 @@ func WithSharedMemory(ctx context.Context) context.Context { // IsSharedMemory returns true if memory should be shared across users. func IsSharedMemory(ctx context.Context) bool { - v, _ := ctx.Value(SharedMemoryKey).(bool) - return v + if v, _ := ctx.Value(SharedMemoryKey).(bool); v { + return true + } + if rc := RunContextFromCtx(ctx); rc != nil { + return rc.SharedMemory + } + return false } // MemoryUserID returns the userID to use for memory operations. @@ -166,8 +194,13 @@ func WithSharedKG(ctx context.Context) context.Context { // IsSharedKG returns true if the knowledge graph should be shared across users. func IsSharedKG(ctx context.Context) bool { - v, _ := ctx.Value(SharedKGKey).(bool) - return v + if v, _ := ctx.Value(SharedKGKey).(bool); v { + return true + } + if rc := RunContextFromCtx(ctx); rc != nil { + return rc.SharedKG + } + return false } // WithLocale returns a new context with the given locale. @@ -191,9 +224,12 @@ func WithTenantID(ctx context.Context, id uuid.UUID) context.Context { // TenantIDFromContext extracts the tenant UUID from context. // Returns uuid.Nil if not set (fail-closed — callers must check). func TenantIDFromContext(ctx context.Context) uuid.UUID { - if v, ok := ctx.Value(TenantIDKey).(uuid.UUID); ok { + if v, ok := ctx.Value(TenantIDKey).(uuid.UUID); ok && v != uuid.Nil { return v } + if rc := RunContextFromCtx(ctx); rc != nil { + return rc.TenantID + } return uuid.Nil } diff --git a/internal/store/run_context.go b/internal/store/run_context.go new file mode 100644 index 00000000..bc6212d8 --- /dev/null +++ b/internal/store/run_context.go @@ -0,0 +1,66 @@ +package store + +import ( + "context" + + "github.com/google/uuid" + + "github.com/nextlevelbuilder/goclaw/internal/config" + "github.com/nextlevelbuilder/goclaw/internal/sandbox" +) + +// runContextKey is the context key for RunContext. +type runContextKey struct{} + +// RunContext consolidates all agent-loop-injected context values into a single +// typed struct. This replaces 27 individual context.WithValue calls with one +// WithRunContext call, improving readability and making it trivial to add new +// scope fields (e.g. ProjectID). +// +// Consumers can read via RunContextFromCtx() or continue using existing +// accessor functions (which fall back to individual keys when RunContext is absent). +type RunContext struct { + // Identity + AgentID uuid.UUID + AgentKey string + TenantID uuid.UUID + UserID string + AgentType string + SenderID string + + // Flags + SelfEvolve bool + SharedMemory bool + SharedKG bool + RestrictToWorkspace bool + + // Tool configuration + BuiltinToolSettings map[string][]byte + ChannelType string + SubagentsCfg *config.SubagentsConfig + ParentModel string + ParentProvider string + MemoryCfg *config.MemoryConfig + SandboxCfg *sandbox.Config + ShellDenyGroups map[string]bool + + // Workspace + Workspace string + TeamWorkspace string + TeamID string + WorkspaceChannel string + WorkspaceChatID string + TeamTaskID string + AgentToolKey string // tool-level agent key for registry routing +} + +// WithRunContext stores a RunContext on the context. +func WithRunContext(ctx context.Context, rc *RunContext) context.Context { + return context.WithValue(ctx, runContextKey{}, rc) +} + +// RunContextFromCtx extracts RunContext from context. Returns nil if not set. +func RunContextFromCtx(ctx context.Context) *RunContext { + rc, _ := ctx.Value(runContextKey{}).(*RunContext) + return rc +} diff --git a/internal/tools/context_keys.go b/internal/tools/context_keys.go index 95e96f4c..37818850 100644 --- a/internal/tools/context_keys.go +++ b/internal/tools/context_keys.go @@ -9,6 +9,7 @@ import ( "github.com/nextlevelbuilder/goclaw/internal/config" "github.com/nextlevelbuilder/goclaw/internal/sandbox" + "github.com/nextlevelbuilder/goclaw/internal/store" ) // Tool execution context keys. @@ -59,8 +60,13 @@ func WithToolChannelType(ctx context.Context, channelType string) context.Contex } func ToolChannelTypeFromCtx(ctx context.Context) string { - v, _ := ctx.Value(ctxChannelType).(string) - return v + if v, _ := ctx.Value(ctxChannelType).(string); v != "" { + return v + } + if rc := store.RunContextFromCtx(ctx); rc != nil { + return rc.ChannelType + } + return "" } func WithToolChatID(ctx context.Context, chatID string) context.Context { @@ -115,8 +121,13 @@ func WithToolWorkspace(ctx context.Context, ws string) context.Context { } func ToolWorkspaceFromCtx(ctx context.Context) string { - v, _ := ctx.Value(ctxWorkspace).(string) - return v + if v, _ := ctx.Value(ctxWorkspace).(string); v != "" { + return v + } + if rc := store.RunContextFromCtx(ctx); rc != nil { + return rc.Workspace + } + return "" } // WithToolAgentKey injects the calling agent's key into context. @@ -127,8 +138,13 @@ func WithToolAgentKey(ctx context.Context, key string) context.Context { } func ToolAgentKeyFromCtx(ctx context.Context) string { - v, _ := ctx.Value(ctxAgentKey).(string) - return v + if v, _ := ctx.Value(ctxAgentKey).(string); v != "" { + return v + } + if rc := store.RunContextFromCtx(ctx); rc != nil { + return rc.AgentToolKey + } + return "" } // WithToolSessionKey injects the parent's session key so subagent announce @@ -170,8 +186,13 @@ func WithBuiltinToolSettings(ctx context.Context, settings BuiltinToolSettings) } func BuiltinToolSettingsFromCtx(ctx context.Context) BuiltinToolSettings { - v, _ := ctx.Value(ctxBuiltinToolSettings).(BuiltinToolSettings) - return v + if v, _ := ctx.Value(ctxBuiltinToolSettings).(BuiltinToolSettings); v != nil { + return v + } + if rc := store.RunContextFromCtx(ctx); rc != nil { + return BuiltinToolSettings(rc.BuiltinToolSettings) + } + return nil } // --- Per-agent restrict_to_workspace override --- @@ -206,8 +227,13 @@ func WithParentModel(ctx context.Context, model string) context.Context { // ParentModelFromCtx returns the parent agent's model from context. func ParentModelFromCtx(ctx context.Context) string { - v, _ := ctx.Value(ctxParentModel).(string) - return v + if v, _ := ctx.Value(ctxParentModel).(string); v != "" { + return v + } + if rc := store.RunContextFromCtx(ctx); rc != nil { + return rc.ParentModel + } + return "" } // --- Parent agent provider (for subagent inheritance) --- @@ -221,8 +247,13 @@ func WithParentProvider(ctx context.Context, providerName string) context.Contex // ParentProviderFromCtx returns the parent agent's provider name from context. func ParentProviderFromCtx(ctx context.Context) string { - v, _ := ctx.Value(ctxParentProvider).(string) - return v + if v, _ := ctx.Value(ctxParentProvider).(string); v != "" { + return v + } + if rc := store.RunContextFromCtx(ctx); rc != nil { + return rc.ParentProvider + } + return "" } // --- Per-agent subagent config override --- @@ -234,8 +265,13 @@ func WithSubagentConfig(ctx context.Context, cfg *config.SubagentsConfig) contex } func SubagentConfigFromCtx(ctx context.Context) *config.SubagentsConfig { - v, _ := ctx.Value(ctxSubagentCfg).(*config.SubagentsConfig) - return v + if v, _ := ctx.Value(ctxSubagentCfg).(*config.SubagentsConfig); v != nil { + return v + } + if rc := store.RunContextFromCtx(ctx); rc != nil { + return rc.SubagentsCfg + } + return nil } // --- Per-agent memory config override --- @@ -247,8 +283,13 @@ func WithMemoryConfig(ctx context.Context, cfg *config.MemoryConfig) context.Con } func MemoryConfigFromCtx(ctx context.Context) *config.MemoryConfig { - v, _ := ctx.Value(ctxMemoryCfg).(*config.MemoryConfig) - return v + if v, _ := ctx.Value(ctxMemoryCfg).(*config.MemoryConfig); v != nil { + return v + } + if rc := store.RunContextFromCtx(ctx); rc != nil { + return rc.MemoryCfg + } + return nil } // --- Team ID propagation (task dispatch → workspace tools) --- @@ -264,8 +305,13 @@ func WithToolTeamID(ctx context.Context, teamID string) context.Context { // ToolTeamIDFromCtx returns the dispatching team's ID from context. func ToolTeamIDFromCtx(ctx context.Context) string { - v, _ := ctx.Value(ctxTeamID).(string) - return v + if v, _ := ctx.Value(ctxTeamID).(string); v != "" { + return v + } + if rc := store.RunContextFromCtx(ctx); rc != nil { + return rc.TeamID + } + return "" } // --- Team workspace path (accessible but not default) --- @@ -280,8 +326,13 @@ func WithToolTeamWorkspace(ctx context.Context, dir string) context.Context { // ToolTeamWorkspaceFromCtx returns the team shared workspace directory path. func ToolTeamWorkspaceFromCtx(ctx context.Context) string { - v, _ := ctx.Value(ctxTeamWorkspace).(string) - return v + if v, _ := ctx.Value(ctxTeamWorkspace).(string); v != "" { + return v + } + if rc := store.RunContextFromCtx(ctx); rc != nil { + return rc.TeamWorkspace + } + return "" } // --- Team task ID propagation (delegation origin → workspace tools) --- @@ -296,8 +347,13 @@ func WithTeamTaskID(ctx context.Context, taskID string) context.Context { // TeamTaskIDFromCtx returns the delegation's team task ID from context. func TeamTaskIDFromCtx(ctx context.Context) string { - v, _ := ctx.Value(ctxTeamTaskID).(string) - return v + if v, _ := ctx.Value(ctxTeamTaskID).(string); v != "" { + return v + } + if rc := store.RunContextFromCtx(ctx); rc != nil { + return rc.TeamTaskID + } + return "" } // --- Workspace scope propagation (delegation origin) --- @@ -312,8 +368,13 @@ func WithWorkspaceChannel(ctx context.Context, channel string) context.Context { } func WorkspaceChannelFromCtx(ctx context.Context) string { - v, _ := ctx.Value(ctxWsChannel).(string) - return v + if v, _ := ctx.Value(ctxWsChannel).(string); v != "" { + return v + } + if rc := store.RunContextFromCtx(ctx); rc != nil { + return rc.WorkspaceChannel + } + return "" } func WithWorkspaceChatID(ctx context.Context, chatID string) context.Context { @@ -321,8 +382,13 @@ func WithWorkspaceChatID(ctx context.Context, chatID string) context.Context { } func WorkspaceChatIDFromCtx(ctx context.Context) string { - v, _ := ctx.Value(ctxWsChatID).(string) - return v + if v, _ := ctx.Value(ctxWsChatID).(string); v != "" { + return v + } + if rc := store.RunContextFromCtx(ctx); rc != nil { + return rc.WorkspaceChatID + } + return "" } // --- Pending team task dispatch (post-turn processing) --- @@ -481,6 +547,11 @@ func WithSandboxConfig(ctx context.Context, cfg *sandbox.Config) context.Context } func SandboxConfigFromCtx(ctx context.Context) *sandbox.Config { - v, _ := ctx.Value(ctxSandboxCfg).(*sandbox.Config) - return v + if v, _ := ctx.Value(ctxSandboxCfg).(*sandbox.Config); v != nil { + return v + } + if rc := store.RunContextFromCtx(ctx); rc != nil { + return rc.SandboxCfg + } + return nil }