From 9b4651c612f1cd57346312b6ec8df9b4e5214d9d Mon Sep 17 00:00:00 2001 From: Harshit Singh Bhandari Date: Thu, 11 Jun 2026 21:49:22 +0530 Subject: [PATCH] fix(session-manager): deliver orchestrator instructions via system prompt (#189) Orchestrator role definitions and worker coordination hints were being prepended/appended to the user-facing prompt string. They now go into SystemPrompt in LaunchConfig so agents receive them as standing instructions rather than part of the human's task request. Closes #182 Co-authored-by: Claude Sonnet 4.6 --- backend/internal/session_manager/manager.go | 34 +++++----- .../internal/session_manager/manager_test.go | 62 ++++++++++++++----- 2 files changed, 61 insertions(+), 35 deletions(-) diff --git a/backend/internal/session_manager/manager.go b/backend/internal/session_manager/manager.go index d30b2b0f1..2d706a6c4 100644 --- a/backend/internal/session_manager/manager.go +++ b/backend/internal/session_manager/manager.go @@ -163,7 +163,7 @@ func (m *Manager) Spawn(ctx context.Context, cfg ports.SpawnConfig) (domain.Sess // so a project can default workers to one agent and orchestrators to another. cfg.Harness = effectiveHarness(cfg.Harness, cfg.Kind, project.Config) - prompt, err := m.buildSpawnPrompt(ctx, cfg) + prompt, systemPrompt, err := m.buildSpawnTexts(ctx, cfg) if err != nil { return domain.SessionRecord{}, fmt.Errorf("spawn: prompt: %w", err) } @@ -220,6 +220,7 @@ func (m *Manager) Spawn(ctx context.Context, cfg ports.SpawnConfig) (domain.Sess SessionID: string(id), WorkspacePath: ws.Path, Prompt: prompt, + SystemPrompt: systemPrompt, IssueID: string(cfg.IssueID), Config: effectiveAgentConfig(cfg.Kind, project.Config), }) @@ -578,22 +579,26 @@ func buildPrompt(cfg ports.SpawnConfig) string { return cfg.Prompt } -func (m *Manager) buildSpawnPrompt(ctx context.Context, cfg ports.SpawnConfig) (string, error) { - prompt := buildPrompt(cfg) +// 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. +func (m *Manager) buildSpawnTexts(ctx context.Context, cfg ports.SpawnConfig) (prompt, systemPrompt string, err error) { + prompt = buildPrompt(cfg) switch cfg.Kind { case domain.KindOrchestrator: - return appendPromptSection(orchestratorPrompt(cfg.ProjectID), prompt), nil + systemPrompt = orchestratorPrompt(cfg.ProjectID) case domain.KindWorker: - orchestratorID, ok, err := m.activeOrchestratorSessionID(ctx, cfg.ProjectID) - if err != nil { - return "", err + orchestratorID, ok, lookupErr := m.activeOrchestratorSessionID(ctx, cfg.ProjectID) + if lookupErr != nil { + return "", "", lookupErr } if ok { - prompt = appendPromptSection(prompt, workerOrchestratorPrompt(orchestratorID)) + systemPrompt = workerOrchestratorPrompt(orchestratorID) } } - return prompt, nil + return prompt, systemPrompt, nil } func (m *Manager) activeOrchestratorSessionID(ctx context.Context, project domain.ProjectID) (domain.SessionID, bool, error) { @@ -632,17 +637,6 @@ An active orchestrator session exists for this project. If you hit a true blocke Only ping the orchestrator for true blockers, cross-session coordination, or decisions that cannot be resolved within your own task.`, orchestratorID) } -func appendPromptSection(prompt, section string) string { - switch { - case prompt == "": - return section - case section == "": - return prompt - default: - return prompt + "\n\n" + section - } -} - // spawnEnv builds the runtime environment: the per-project env vars first, then // the AO-internal vars last so they always win (a project cannot override // AO_SESSION_ID and friends). diff --git a/backend/internal/session_manager/manager_test.go b/backend/internal/session_manager/manager_test.go index 1f742e670..779340fa6 100644 --- a/backend/internal/session_manager/manager_test.go +++ b/backend/internal/session_manager/manager_test.go @@ -155,10 +155,12 @@ func (fakeAgents) Agent(domain.AgentHarness) (ports.Agent, bool) { return fakeAg type recordingAgent struct { fakeAgent lastConfig ports.AgentConfig + lastLaunch ports.LaunchConfig } func (a *recordingAgent) GetLaunchCommand(_ context.Context, cfg ports.LaunchConfig) ([]string, error) { a.lastConfig = cfg.Config + a.lastLaunch = cfg return []string{"launch"}, nil } @@ -496,59 +498,89 @@ func TestSpawn_DefaultsBranchFromSessionID(t *testing.T) { } func TestSpawnWorker_AppendsActiveOrchestratorContact(t *testing.T) { - m, st, _, _ := newManager() + st := newFakeStore() st.num = 1 st.sessions["mer-1"] = domain.SessionRecord{ID: "mer-1", ProjectID: "mer", Kind: domain.KindOrchestrator} + agent := &recordingAgent{} + rt := &fakeRuntime{} + ws := &fakeWorkspace{} + lookPath := func(string) (string, error) { return "/bin/true", nil } + m := New(Deps{Runtime: rt, Agents: singleAgent{agent: agent}, Workspace: ws, Store: st, Messenger: &fakeMessenger{}, Lifecycle: &fakeLCM{store: st}, LookPath: lookPath}) s, err := m.Spawn(ctx, ports.SpawnConfig{ProjectID: "mer", Kind: domain.KindWorker, Prompt: "do it"}) if err != nil { t.Fatal(err) } - prompt := st.sessions[s.ID].Metadata.Prompt + + // The user prompt must be preserved and stored in metadata as-is. + if got := st.sessions[s.ID].Metadata.Prompt; got != "do it" { + t.Fatalf("metadata prompt = %q, want %q", got, "do it") + } + + // Coordination instructions must be in the system prompt, not the user prompt. + systemPrompt := agent.lastLaunch.SystemPrompt for _, want := range []string{ - "do it", "## Orchestrator coordination", `ao send --session mer-1 --message ""`, "Only ping the orchestrator for true blockers, cross-session coordination", } { - if !strings.Contains(prompt, want) { - t.Fatalf("prompt missing %q:\n%s", want, prompt) + if !strings.Contains(systemPrompt, want) { + t.Fatalf("system prompt missing %q:\n%s", want, systemPrompt) } } + if strings.Contains(agent.lastLaunch.Prompt, "## Orchestrator coordination") { + t.Fatalf("orchestrator coordination must not be in the user prompt:\n%s", agent.lastLaunch.Prompt) + } } func TestSpawnWorker_SkipsTerminatedOrchestratorContact(t *testing.T) { - m, st, _, _ := newManager() + st := newFakeStore() st.num = 1 st.sessions["mer-1"] = domain.SessionRecord{ID: "mer-1", ProjectID: "mer", Kind: domain.KindOrchestrator, IsTerminated: true} + agent := &recordingAgent{} + rt := &fakeRuntime{} + ws := &fakeWorkspace{} + lookPath := func(string) (string, error) { return "/bin/true", nil } + m := New(Deps{Runtime: rt, Agents: singleAgent{agent: agent}, Workspace: ws, Store: st, Messenger: &fakeMessenger{}, Lifecycle: &fakeLCM{store: st}, LookPath: lookPath}) - s, err := m.Spawn(ctx, ports.SpawnConfig{ProjectID: "mer", Kind: domain.KindWorker, Prompt: "do it"}) + _, err := m.Spawn(ctx, ports.SpawnConfig{ProjectID: "mer", Kind: domain.KindWorker, Prompt: "do it"}) if err != nil { t.Fatal(err) } - prompt := st.sessions[s.ID].Metadata.Prompt - if strings.Contains(prompt, "## Orchestrator coordination") || strings.Contains(prompt, "ao send --session mer-1") { - t.Fatalf("terminated orchestrator should not be added to prompt:\n%s", prompt) + systemPrompt := agent.lastLaunch.SystemPrompt + if strings.Contains(systemPrompt, "## Orchestrator coordination") || strings.Contains(systemPrompt, "ao send --session mer-1") { + t.Fatalf("terminated orchestrator should not be added to system prompt:\n%s", systemPrompt) } } func TestSpawnOrchestrator_UsesCoordinatorPrompt(t *testing.T) { - m, st, _, _ := newManager() - s, err := m.Spawn(ctx, ports.SpawnConfig{ProjectID: "mer", Kind: domain.KindOrchestrator}) + st := newFakeStore() + agent := &recordingAgent{} + rt := &fakeRuntime{} + ws := &fakeWorkspace{} + lookPath := func(string) (string, error) { return "/bin/true", nil } + m := New(Deps{Runtime: rt, Agents: singleAgent{agent: agent}, Workspace: ws, Store: st, Messenger: &fakeMessenger{}, Lifecycle: &fakeLCM{store: st}, LookPath: lookPath}) + + _, err := m.Spawn(ctx, ports.SpawnConfig{ProjectID: "mer", Kind: domain.KindOrchestrator}) if err != nil { t.Fatal(err) } - prompt := st.sessions[s.ID].Metadata.Prompt + + // Coordinator instructions must be in the system prompt, not the user prompt. + systemPrompt := agent.lastLaunch.SystemPrompt for _, want := range []string{ "You are the human-facing coordinator for project mer", `ao spawn --project mer --prompt ""`, "`ao send`", "avoid doing implementation yourself unless it is necessary", } { - if !strings.Contains(prompt, want) { - t.Fatalf("prompt missing %q:\n%s", want, prompt) + if !strings.Contains(systemPrompt, want) { + t.Fatalf("system prompt missing %q:\n%s", want, systemPrompt) } } + if strings.Contains(agent.lastLaunch.Prompt, "You are the human-facing coordinator") { + t.Fatalf("coordinator role must not be in the user prompt:\n%s", agent.lastLaunch.Prompt) + } } // TestRestore_RefusesIncompleteHandle covers Bug 2: a terminated row whose