Skip to content

Commit 06b463e

Browse files
ianhodgebnavetta
andauthored
Adding customizable idle-on-complete for self hosted worker (#35)
Co-authored-by: Ben Navetta <ben@warp.dev>
1 parent a0e9c9b commit 06b463e

5 files changed

Lines changed: 72 additions & 8 deletions

File tree

internal/common/task_utils.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,17 @@ import (
77
"github.com/warpdotdev/oz-agent-worker/internal/types"
88
)
99

10+
// TaskAugmentOptions contains worker-level settings that are translated into oz CLI flags
11+
// for every task. Add new per-worker CLI overrides here rather than as extra parameters.
12+
type TaskAugmentOptions struct {
13+
// IdleOnComplete is passed to --idle-on-complete. Empty string uses the oz CLI default
14+
// (45m). Use "0s" to exit immediately after the conversation finishes.
15+
IdleOnComplete string
16+
}
17+
1018
// AugmentArgsForTask allows different task sources to add CLI args in a centralized place.
1119
// Uses task.AgentConfigSnapshot as the source of truth when available.
12-
func AugmentArgsForTask(task *types.Task, args []string) []string {
20+
func AugmentArgsForTask(task *types.Task, args []string, opts TaskAugmentOptions) []string {
1321
if task == nil {
1422
return args
1523
}
@@ -57,8 +65,14 @@ func AugmentArgsForTask(task *types.Task, args []string) []string {
5765
}
5866
}
5967

60-
// Keep the agent alive after task completion to allow followups.
61-
args = append(args, "--idle-on-complete")
68+
// Keep the agent alive after task completion to allow follow-ups.
69+
// If no duration is configured, pass the flag without a value so the oz CLI
70+
// uses its default of 45 minutes.
71+
if opts.IdleOnComplete == "" {
72+
args = append(args, "--idle-on-complete")
73+
} else {
74+
args = append(args, "--idle-on-complete", opts.IdleOnComplete)
75+
}
6276

6377
return args
6478
}

internal/config/config.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,15 @@ import (
1414

1515
// FileConfig represents the top-level YAML configuration file.
1616
type FileConfig struct {
17-
WorkerID string `yaml:"worker_id"`
18-
Cleanup *bool `yaml:"cleanup"`
19-
MaxConcurrentTasks *int `yaml:"max_concurrent_tasks"`
20-
Backend BackendConfig `yaml:"backend"`
17+
WorkerID string `yaml:"worker_id"`
18+
Cleanup *bool `yaml:"cleanup"`
19+
MaxConcurrentTasks *int `yaml:"max_concurrent_tasks"`
20+
// IdleOnComplete controls how long the oz CLI process stays alive after a task's
21+
// conversation finishes, to allow follow-up interactions via the shared session.
22+
// Uses humantime format (e.g. "45m", "10m", "0s"). When nil, the oz CLI default
23+
// of 45 minutes is used.
24+
IdleOnComplete *string `yaml:"idle_on_complete"`
25+
Backend BackendConfig `yaml:"backend"`
2126
}
2227

2328
// BackendConfig contains the backend selection.

internal/config/config_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,38 @@ func TestLoadFileNotFound(t *testing.T) {
257257
}
258258
}
259259

260+
func TestLoadIdleOnComplete(t *testing.T) {
261+
t.Run("parses idle_on_complete when set", func(t *testing.T) {
262+
path := writeTestConfig(t, `
263+
worker_id: "test"
264+
idle_on_complete: "10m"
265+
`)
266+
cfg, err := Load(path)
267+
if err != nil {
268+
t.Fatalf("unexpected error: %v", err)
269+
}
270+
if cfg.IdleOnComplete == nil {
271+
t.Fatal("expected idle_on_complete to be set")
272+
}
273+
if *cfg.IdleOnComplete != "10m" {
274+
t.Errorf("idle_on_complete = %q, want %q", *cfg.IdleOnComplete, "10m")
275+
}
276+
})
277+
278+
t.Run("idle_on_complete is nil when not set", func(t *testing.T) {
279+
path := writeTestConfig(t, `
280+
worker_id: "test"
281+
`)
282+
cfg, err := Load(path)
283+
if err != nil {
284+
t.Fatalf("unexpected error: %v", err)
285+
}
286+
if cfg.IdleOnComplete != nil {
287+
t.Errorf("expected idle_on_complete to be nil, got %q", *cfg.IdleOnComplete)
288+
}
289+
})
290+
}
291+
260292
func TestLoadMaxConcurrentTasks(t *testing.T) {
261293
t.Run("parses max_concurrent_tasks when set", func(t *testing.T) {
262294
path := writeTestConfig(t, `

internal/worker/worker.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ type Config struct {
3333
LogLevel string
3434
BackendType string // "docker" or "direct"
3535
MaxConcurrentTasks int // 0 means unlimited
36+
// IdleOnComplete is passed to the oz CLI's --idle-on-complete flag for every task.
37+
// Empty string means use the oz CLI default (45m). Use "0s" to disable idle.
38+
IdleOnComplete string
3639

3740
// Backend-specific configs. Only the one matching BackendType should be set.
3841
Docker *DockerBackendConfig
@@ -371,7 +374,9 @@ func (w *Worker) prepareTaskParams(assignment *types.TaskAssignmentMessage) *Tas
371374
"--server-root-url",
372375
w.config.ServerRootURL,
373376
}
374-
baseArgs = common.AugmentArgsForTask(task, baseArgs)
377+
baseArgs = common.AugmentArgsForTask(task, baseArgs, common.TaskAugmentOptions{
378+
IdleOnComplete: w.config.IdleOnComplete,
379+
})
375380

376381
// Build a unified sidecar list: the Warp agent sidecar (mounted at /agent, where
377382
// entrypoint.sh lives) comes first, followed by any additional sidecars.

main.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ var CLI struct {
2626
Volumes []string `help:"Volume mounts for task containers (format: HOST_PATH:CONTAINER_PATH or HOST_PATH:CONTAINER_PATH:MODE)" short:"v"`
2727
Env []string `help:"Environment variables for task containers (format: KEY=VALUE or KEY to pass through from host)" short:"e"`
2828
MaxConcurrentTasks int `help:"Maximum number of tasks to run concurrently (0 for unlimited)" default:"0"`
29+
IdleOnComplete string `help:"How long to keep the oz agent alive after a task completes, for follow-ups (e.g. 45m, 10m, 0s). Defaults to 45m when not set."`
2930
}
3031

3132
func main() {
@@ -123,6 +124,12 @@ func mergeConfig(fileConfig *config.FileConfig) (worker.Config, error) {
123124
maxConcurrentTasks = *fileConfig.MaxConcurrentTasks
124125
}
125126

127+
// Resolve idle_on_complete: CLI (non-empty) > config file > "" (oz CLI default = 45m).
128+
idleOnComplete := CLI.IdleOnComplete
129+
if idleOnComplete == "" && fileConfig != nil && fileConfig.IdleOnComplete != nil {
130+
idleOnComplete = *fileConfig.IdleOnComplete
131+
}
132+
126133
wc := worker.Config{
127134
APIKey: CLI.APIKey,
128135
WorkerID: workerID,
@@ -131,6 +138,7 @@ func mergeConfig(fileConfig *config.FileConfig) (worker.Config, error) {
131138
LogLevel: CLI.LogLevel,
132139
BackendType: backendType,
133140
MaxConcurrentTasks: maxConcurrentTasks,
141+
IdleOnComplete: idleOnComplete,
134142
}
135143

136144
switch backendType {

0 commit comments

Comments
 (0)