Skip to content
Merged
52 changes: 29 additions & 23 deletions cmd/harness/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -382,18 +382,21 @@ func runCmd(args []string) error {
lateAPI.Bind(newLazyRunClientAPI(func() *engine.Session { return sess }))

s, err := resolveSession(engine.Config{
Providers: registry(cfg),
Model: model,
System: systemPrompt(workDir, opts.system),
MaxTokens: opts.maxTokens,
WorkDir: workDir,
SessionDir: sesDir,
OnEvent: onEvent,
Instructions: instructionsConfig(cfg, opts.noInstructions),
SkillsDirs: skillsDirs(cfg, opts.skillsDirs, workDir),
Hooks: pluginHooks(host),
MCP: mcpRegistry(mcpMgr),
Processes: processRegistry(procMgr),
Providers: registry(cfg),
Model: model,
System: systemPrompt(workDir, opts.system),
MaxTokens: opts.maxTokens,
WorkDir: workDir,
SessionDir: sesDir,
OnEvent: onEvent,
Instructions: instructionsConfig(cfg, opts.noInstructions),
SkillsDirs: skillsDirs(cfg, opts.skillsDirs, workDir),
Hooks: pluginHooks(host),
MCP: mcpRegistry(mcpMgr),
Processes: processRegistry(procMgr),
ContextWindowTokens: cfg.ContextWindowTokens,
CompactionThreshold: cfg.CompactionThreshold,
CompactionKeepTurns: cfg.CompactionKeepTurns,
}, opts.resume, opts.cont, modelSet)
if err != nil {
return err
Expand Down Expand Up @@ -693,17 +696,20 @@ func serveCmd(args []string) error {
var srv *server.Server
mkCfg := func(model message.ModelRef) engine.Config {
return engine.Config{
Providers: reg,
Model: model,
System: systemPrompt(workDir, ""),
WorkDir: workDir,
SessionDir: sesDir,
OnEvent: func(ev engine.Event) { srv.Publish(ev) },
Instructions: instructionsConfig(cfg, noInstructions),
SkillsDirs: skillsDirs(cfg, skillDirs, workDir),
Hooks: pluginHooks(pluginHost),
MCP: mcpRegistry(mcpMgr),
Processes: processRegistry(procMgr),
Providers: reg,
Model: model,
System: systemPrompt(workDir, ""),
WorkDir: workDir,
SessionDir: sesDir,
OnEvent: func(ev engine.Event) { srv.Publish(ev) },
Instructions: instructionsConfig(cfg, noInstructions),
SkillsDirs: skillsDirs(cfg, skillDirs, workDir),
Hooks: pluginHooks(pluginHost),
MCP: mcpRegistry(mcpMgr),
Processes: processRegistry(procMgr),
ContextWindowTokens: cfg.ContextWindowTokens,
CompactionThreshold: cfg.CompactionThreshold,
CompactionKeepTurns: cfg.CompactionKeepTurns,
}
}
srv, err = server.New(server.Options{
Expand Down
24 changes: 24 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,21 @@ type Config struct {
// configures no processes. Merge rules mirror MCPServers: keys merge,
// but a same-name project entry replaces the user entry wholesale.
Processes map[string]ProcessSpec `json:"processes,omitempty"`
// ContextWindowTokens sets engine.Config.ContextWindowTokens for every
// session this process creates: the model's context window size, in
// tokens. Zero (omitted, the default) disables automatic compaction
// entirely — see docs/design/context-compaction.md and issue #62 layer
// 3. Opt-in: the engine has no built-in per-model table.
ContextWindowTokens int `json:"context_window_tokens,omitempty"`
// CompactionThreshold sets engine.Config.CompactionThreshold: the
// fraction of ContextWindowTokens at which automatic compaction
// triggers. Zero (omitted) defaults to 0.8 (see the engine).
CompactionThreshold float64 `json:"compaction_threshold,omitempty"`
// CompactionKeepTurns sets engine.Config.CompactionKeepTurns: how many
// of the most recent turns automatic compaction always keeps verbatim.
// Zero (omitted) defaults to 2 (see the engine); the effective value
// can never go below 1.
CompactionKeepTurns int `json:"compaction_keep_turns,omitempty"`
}

// ProcessSpec configures one managed process (package engine's
Expand Down Expand Up @@ -520,6 +535,15 @@ func merge(base, over *Config) *Config {
if over.GoalEvaluatorModel != "" {
out.GoalEvaluatorModel = over.GoalEvaluatorModel
}
if over.ContextWindowTokens != 0 {
out.ContextWindowTokens = over.ContextWindowTokens
}
if over.CompactionThreshold != 0 {
out.CompactionThreshold = over.CompactionThreshold
}
if over.CompactionKeepTurns != 0 {
out.CompactionKeepTurns = over.CompactionKeepTurns
}
// Arrays override wholesale: a non-empty project value replaces the user
// value entirely; otherwise inherit. Copy so the merged config never
// aliases either input's slice.
Expand Down
31 changes: 31 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,37 @@ func TestLoadSkillsDirs(t *testing.T) {
})
}

// TestMergeCompactionFields is the red-first test for docs/design/context-
// compaction.md's config fields: project non-zero values override the user
// layer, same scalar-override rule as GoalEvaluatorModel.
func TestMergeCompactionFields(t *testing.T) {
base := &Config{ContextWindowTokens: 100000, CompactionThreshold: 0.9, CompactionKeepTurns: 3}
t.Run("zero project values inherit the user layer", func(t *testing.T) {
got := merge(base, &Config{})
if got.ContextWindowTokens != 100000 {
t.Errorf("ContextWindowTokens = %d, want inherited 100000", got.ContextWindowTokens)
}
if got.CompactionThreshold != 0.9 {
t.Errorf("CompactionThreshold = %v, want inherited 0.9", got.CompactionThreshold)
}
if got.CompactionKeepTurns != 3 {
t.Errorf("CompactionKeepTurns = %d, want inherited 3", got.CompactionKeepTurns)
}
})
t.Run("non-zero project values override", func(t *testing.T) {
got := merge(base, &Config{ContextWindowTokens: 50000, CompactionThreshold: 0.7, CompactionKeepTurns: 1})
if got.ContextWindowTokens != 50000 {
t.Errorf("ContextWindowTokens = %d, want project override 50000", got.ContextWindowTokens)
}
if got.CompactionThreshold != 0.7 {
t.Errorf("CompactionThreshold = %v, want project override 0.7", got.CompactionThreshold)
}
if got.CompactionKeepTurns != 1 {
t.Errorf("CompactionKeepTurns = %d, want project override 1", got.CompactionKeepTurns)
}
})
}

func TestMergeSkillsDirs(t *testing.T) {
t.Run("non-empty project overrides user entirely", func(t *testing.T) {
base := &Config{SkillsDirs: []string{"user/a", "user/b"}}
Expand Down
Loading