refactor(store): introduce typed RunContext for consolidated context injection

Add store.RunContext struct that consolidates 27 loop-injected context
values into a single typed struct. RunContext is built once in
loop_context.go and injected via store.WithRunContext(ctx, rc).

24 accessor functions (10 in store/context.go, 14 in tools/context_keys.go)
updated with RunContext fallback — check individual key first, then
RunContext. Consumers (170+ files) require zero changes.

This enables adding new scope fields (e.g. ProjectID) by adding a single
struct field instead of a new context key + With/From function pair.
This commit is contained in:
viettranx
2026-03-25 09:49:55 +07:00
parent d3a4398e78
commit 3bb524cb39
4 changed files with 251 additions and 42 deletions
+36
View File
@@ -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,
+50 -14
View File
@@ -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
}
+66
View File
@@ -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
}
+99 -28
View File
@@ -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
}