Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 4 additions & 11 deletions backend/internal/session_manager/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -594,25 +594,18 @@ func buildPrompt(cfg ports.SpawnConfig) string {
return cfg.Prompt
}

// orchestratorKickoffPrompt is the default first turn for an orchestrator
// spawned without an explicit prompt. The role definition rides the system
// prompt, and an interactive agent launched with only a system prompt sits at
// an empty input box; this gives it a turn to act on.
const orchestratorKickoffPrompt = "Get oriented: review the current repo state and any active worker sessions, then report your status and wait for direction."

// buildSpawnTexts returns the user-facing prompt and the system prompt to
// deliver separately to the agent. Orchestrator role instructions and worker
// coordination hints are placed in the system prompt so they are treated as
// standing instructions rather than part of the human's task request.
// standing instructions rather than part of the human's task request. A
// promptless spawn delivers no user prompt at all: the agent simply lands at an
// empty input box rather than receiving an auto-generated kickoff turn.
func (m *Manager) buildSpawnTexts(ctx context.Context, cfg ports.SpawnConfig) (prompt, systemPrompt string, err error) {
prompt = buildPrompt(cfg)
systemPrompt, err = m.buildSystemPrompt(ctx, cfg.Kind, cfg.ProjectID)
if err != nil {
return "", "", err
}
if cfg.Kind == domain.KindOrchestrator && prompt == "" {
prompt = orchestratorKickoffPrompt
}
return prompt, systemPrompt, nil
}

Expand Down Expand Up @@ -858,7 +851,7 @@ func restoreArgv(ctx context.Context, agent ports.Agent, id domain.SessionID, wo
WorkspacePath: workspacePath,
Metadata: map[string]string{ports.MetadataKeyAgentSessionID: meta.AgentSessionID},
}
cmd, ok, err := agent.GetRestoreCommand(ctx, ports.RestoreConfig{Session: ref, Config: agentConfig, Permissions: agentConfig.Permissions})
cmd, ok, err := agent.GetRestoreCommand(ctx, ports.RestoreConfig{Session: ref, SystemPrompt: systemPrompt, Config: agentConfig, Permissions: agentConfig.Permissions})
if err != nil {
return nil, fmt.Errorf("restore command: %w", err)
}
Expand Down
13 changes: 9 additions & 4 deletions backend/internal/session_manager/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,11 @@ func (a *recordingAgent) GetLaunchCommand(_ context.Context, cfg ports.LaunchCon
func (a *recordingAgent) GetRestoreCommand(_ context.Context, cfg ports.RestoreConfig) ([]string, bool, error) {
a.lastConfig = cfg.Config
a.lastRestore = cfg
// Mirror real adapters: with no native agent-session id to resume, signal
// "cannot restore" so the manager falls back to a fresh launch.
if cfg.Session.Metadata[ports.MetadataKeyAgentSessionID] == "" {
return nil, false, nil
}
return []string{"resume"}, true, nil
}

Expand Down Expand Up @@ -635,10 +640,10 @@ func TestSpawnOrchestrator_UsesCoordinatorPrompt(t *testing.T) {
t.Fatalf("coordinator role must not be in the user prompt:\n%s", agent.lastLaunch.Prompt)
}

// A promptless orchestrator still needs a first turn: with the role in the
// system prompt only, an interactive agent would idle at an empty input box.
if agent.lastLaunch.Prompt != orchestratorKickoffPrompt {
t.Fatalf("prompt = %q, want kick-off prompt", agent.lastLaunch.Prompt)
// A promptless orchestrator gets no auto-generated kickoff turn: spawning
// must deliver nothing to the agent, leaving it idle at an empty input box.
if agent.lastLaunch.Prompt != "" {
t.Fatalf("prompt = %q, want empty (no kickoff turn)", agent.lastLaunch.Prompt)
}
}

Expand Down
Loading