From 8b9fd30cd7674c221af95f59ed6c4d62fa91c635 Mon Sep 17 00:00:00 2001 From: ZanzyTHEbar Date: Wed, 4 Mar 2026 18:44:20 +0000 Subject: [PATCH] feat: update config, session manager, tools dag and subagent --- pkg/config/config.go | 13 +++++++++++++ pkg/session/manager.go | 36 ++++++++++++++++++++++++++++++++++++ pkg/tools/dag.go | 2 +- pkg/tools/subagent.go | 17 +++++++++++++++++ 4 files changed, 67 insertions(+), 1 deletion(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index 4e7c541dc..917ea6e12 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -144,6 +144,14 @@ type ContinuityRetentionConfig struct { FailureKeepMessages int `json:"failure_keep_messages" env:"DRAGONSCALE_AGENTS_DEFAULTS_CONTINUITY_RETENTION_FAILURE_KEEP_MESSAGES"` } +// CompactionConfig controls the dual-threshold compaction control loop (LCM ADR-002). +type CompactionConfig struct { + // SoftThresholdPct triggers async background compaction (default 70). + SoftThresholdPct int `json:"soft_threshold_pct" env:"DRAGONSCALE_AGENTS_DEFAULTS_COMPACTION_SOFT_THRESHOLD_PCT"` + // HardThresholdPct triggers synchronous blocking compaction (default 90). + HardThresholdPct int `json:"hard_threshold_pct" env:"DRAGONSCALE_AGENTS_DEFAULTS_COMPACTION_HARD_THRESHOLD_PCT"` +} + type AgentDefaults struct { // Sandbox is the directory for agent file operations (tools sandbox). // Defaults to $XDG_DATA_HOME/dragonscale/sandbox when empty. @@ -155,6 +163,7 @@ type AgentDefaults struct { Temperature float64 `json:"temperature" env:"DRAGONSCALE_AGENTS_DEFAULTS_TEMPERATURE"` MaxToolIterations int `json:"max_tool_iterations" env:"DRAGONSCALE_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"` ContinuityRetention ContinuityRetentionConfig `json:"continuity_retention"` + Compaction CompactionConfig `json:"compaction"` // Deprecated: Use Sandbox instead. Kept for backward compatibility during migration. Workspace string `json:"workspace,omitempty" env:"DRAGONSCALE_AGENTS_DEFAULTS_WORKSPACE"` @@ -373,6 +382,10 @@ func DefaultConfig() *Config { TargetContextRatio: 0.10, FailureKeepMessages: 10, }, + Compaction: CompactionConfig{ + SoftThresholdPct: 70, + HardThresholdPct: 90, + }, }, }, Channels: ChannelsConfig{ diff --git a/pkg/session/manager.go b/pkg/session/manager.go index 9b20ae977..e2d9b08e5 100644 --- a/pkg/session/manager.go +++ b/pkg/session/manager.go @@ -402,6 +402,27 @@ func (sm *SessionManager) persistMessageToDelegate(sessionKey string, msg messag map[string]interface{}{"session": sessionKey, "error": err.Error()}) return } + + // Dual-write: persist verbatim copy to immutable store (LCM ADR-001). + immutableWriter, ok := sm.delegate.(interface { + InsertImmutableMessage(ctx context.Context, msg *memory.ImmutableMessage) error + }) + if ok { + imMsg := &memory.ImmutableMessage{ + ID: ids.New(), + SessionKey: sessionKey, + Role: msg.Role, + Content: msg.Content, + ToolCallID: msg.ToolCallID, + ToolCalls: toolCallsJSON(msg), + TokenEstimate: estimateTokensSimple(msg.Content), + } + if err := immutableWriter.InsertImmutableMessage(ctx, imMsg); err != nil { + logger.WarnCF("session", "Failed to persist immutable message", + map[string]interface{}{"session": sessionKey, "error": err.Error()}) + } + } + priorPtr, _ := sm.loadProjectionPointer(ctx, sessionKey) newPtr := advancePointer(priorPtr, item.ID, now) if err := sm.persistProjectionPointer(ctx, sessionKey, newPtr); err != nil { @@ -729,3 +750,18 @@ func (sm *SessionManager) SetHistory(key string, history []messages.Message) { session.Updated = time.Now() } } + +func toolCallsJSON(msg messages.Message) string { + if len(msg.ToolCalls) == 0 { + return "" + } + b, err := jsonv2.Marshal(msg.ToolCalls) + if err != nil { + return "" + } + return string(b) +} + +func estimateTokensSimple(content string) int { + return (len(content) + 3) / 4 +} diff --git a/pkg/tools/dag.go b/pkg/tools/dag.go index eef0bcc15..a439da34f 100644 --- a/pkg/tools/dag.go +++ b/pkg/tools/dag.go @@ -29,7 +29,7 @@ type DAGToolDeps struct { SessionFn func() string // returns current session key } -func recallRowToItem(row memsqlc.RecallItem) *memory.RecallItem { +func recallRowToItem(row memsqlc.ListSessionMessagesPagedRow) *memory.RecallItem { return &memory.RecallItem{ ID: row.ID, AgentID: row.AgentID, diff --git a/pkg/tools/subagent.go b/pkg/tools/subagent.go index 3f73da87c..ee92537ff 100644 --- a/pkg/tools/subagent.go +++ b/pkg/tools/subagent.go @@ -3,6 +3,7 @@ package tools import ( "context" "fmt" + "log/slog" "strings" "sync" "time" @@ -186,6 +187,14 @@ func (sm *SubagentManager) Spawn(ctx context.Context, task, label, delegatedScop return "", fmt.Errorf("nested delegation requires delegated_scope and kept_work") } } + if keptWork != "" { + slog.Debug("delegation scope-reduction", + "parent_depth", parentDepth, + "child_depth", childDepth, + "kept_work", keptWork, + "delegated_scope", delegatedScope, + ) + } if sm.runLoop == nil { sm.mu.Unlock() return "", ErrRunLoopNotConfigured @@ -461,6 +470,14 @@ func (t *SubagentTool) Execute(ctx context.Context, args map[string]interface{}) return ErrorResult("nested delegation requires delegated_scope and kept_work") } } + if keptWork != "" { + slog.Debug("delegation scope-reduction", + "parent_depth", parentDepth, + "child_depth", childDepth, + "kept_work", keptWork, + "delegated_scope", delegatedScope, + ) + } systemPrompt := "You are a subagent operating with main-loop control flow. Execute actions via tools, call discovered tools directly, and provide a clear concise result."