From ffdd78c931968b493fdf1a6834d67d6fab504e2f Mon Sep 17 00:00:00 2001 From: whoisasx Date: Mon, 29 Jun 2026 23:25:35 +0530 Subject: [PATCH] refactor: keep session prompts with manager --- backend/internal/session_manager/manager.go | 23 ++++--- .../prompts.go => session_manager/prompt.go} | 62 ++++++++----------- .../prompt_test.go} | 16 ++--- docs/agent/README.md | 2 +- 4 files changed, 45 insertions(+), 58 deletions(-) rename backend/internal/{sessionprompt/prompts.go => session_manager/prompt.go} (84%) rename backend/internal/{sessionprompt/prompts_test.go => session_manager/prompt_test.go} (85%) diff --git a/backend/internal/session_manager/manager.go b/backend/internal/session_manager/manager.go index 0d66eeb67..b1ac8744d 100644 --- a/backend/internal/session_manager/manager.go +++ b/backend/internal/session_manager/manager.go @@ -17,7 +17,6 @@ import ( "github.com/aoagents/agent-orchestrator/backend/internal/domain" "github.com/aoagents/agent-orchestrator/backend/internal/ports" aoprocess "github.com/aoagents/agent-orchestrator/backend/internal/process" - "github.com/aoagents/agent-orchestrator/backend/internal/sessionprompt" ) // Sentinel errors returned by the Session Manager; callers match them with @@ -951,32 +950,32 @@ func defaultSessionBranch(id domain.SessionID, kind domain.SessionKind, prefix s } func buildPrompt(cfg ports.SpawnConfig) string { - return sessionprompt.BuildTaskPrompt(sessionprompt.TaskConfig{ - Role: promptRole(cfg.Kind), + return buildTaskPrompt(taskPromptConfig{ + Role: promptRoleForKind(cfg.Kind), Prompt: cfg.Prompt, IssueID: string(cfg.IssueID), IssueContext: cfg.IssueContext, }) } -func promptRole(kind domain.SessionKind) sessionprompt.Role { +func promptRoleForKind(kind domain.SessionKind) sessionPromptRole { switch kind { case domain.KindOrchestrator: - return sessionprompt.RoleOrchestrator + return sessionPromptRoleOrchestrator case domain.KindWorker: - return sessionprompt.RoleWorker + return sessionPromptRoleWorker default: return "" } } -func promptProjectContext(projectID domain.ProjectID, project domain.ProjectRecord) sessionprompt.ProjectContext { +func promptProjectContext(projectID domain.ProjectID, project domain.ProjectRecord) promptProject { cfg := project.Config.WithDefaults() id := project.ID if strings.TrimSpace(id) == "" { id = string(projectID) } - return sessionprompt.ProjectContext{ + return promptProject{ ID: id, Name: project.DisplayName, Repo: project.RepoOriginURL, @@ -1009,8 +1008,8 @@ func (m *Manager) buildSystemPrompt(ctx context.Context, kind domain.SessionKind if err != nil { return "", err } - role := promptRole(kind) - cfg := sessionprompt.SystemConfig{ + role := promptRoleForKind(kind) + cfg := systemPromptConfig{ Role: role, Project: promptProjectContext(projectID, project), } @@ -1025,7 +1024,7 @@ func (m *Manager) buildSystemPrompt(ctx context.Context, kind domain.SessionKind if ok { cfg.OrchestratorSessionID = string(orchestratorID) } - rules, err := sessionprompt.BuildProjectRules(sessionprompt.RulesConfig{ + rules, err := buildProjectRules(projectRulesConfig{ ProjectPath: project.Path, AgentRules: project.Config.AgentRules, AgentRulesFile: project.Config.AgentRulesFile, @@ -1035,7 +1034,7 @@ func (m *Manager) buildSystemPrompt(ctx context.Context, kind domain.SessionKind } cfg.ProjectRules = rules } - return sessionprompt.BuildSystemPrompt(cfg), nil + return buildSystemPromptText(cfg), nil } func (m *Manager) activeOrchestratorSessionID(ctx context.Context, project domain.ProjectID) (domain.SessionID, bool, error) { diff --git a/backend/internal/sessionprompt/prompts.go b/backend/internal/session_manager/prompt.go similarity index 84% rename from backend/internal/sessionprompt/prompts.go rename to backend/internal/session_manager/prompt.go index 8cf6e65aa..934ce25a3 100644 --- a/backend/internal/sessionprompt/prompts.go +++ b/backend/internal/session_manager/prompt.go @@ -1,4 +1,4 @@ -package sessionprompt +package sessionmanager import ( "fmt" @@ -7,18 +7,14 @@ import ( "strings" ) -// Role names the AO session role whose prompt is being built. -type Role string +type sessionPromptRole string const ( - // RoleOrchestrator identifies the human-facing coordinator session role. - RoleOrchestrator Role = "orchestrator" - // RoleWorker identifies an implementation worker session role. - RoleWorker Role = "worker" + sessionPromptRoleOrchestrator sessionPromptRole = "orchestrator" + sessionPromptRoleWorker sessionPromptRole = "worker" ) -// ProjectContext is the project information safe to include in session prompts. -type ProjectContext struct { +type promptProject struct { ID string Name string Repo string @@ -26,36 +22,31 @@ type ProjectContext struct { Path string } -// TaskConfig carries the per-session work request. It intentionally excludes -// project rules and role instructions; those belong in the system prompt. -type TaskConfig struct { - Role Role +type taskPromptConfig struct { + Role sessionPromptRole Prompt string IssueID string IssueContext string } -// SystemConfig carries standing instructions for the session role. -type SystemConfig struct { - Role Role - Project ProjectContext +type systemPromptConfig struct { + Role sessionPromptRole + Project promptProject OrchestratorSessionID string ProjectRules string OrchestratorRules string } -// RulesConfig points at project-configured worker rules. -type RulesConfig struct { +type projectRulesConfig struct { ProjectPath string AgentRules string AgentRulesFile string } -// BuildTaskPrompt builds the user/task prompt sent at spawn time. -func BuildTaskPrompt(cfg TaskConfig) string { +func buildTaskPrompt(cfg taskPromptConfig) string { issueContext := strings.TrimSpace(cfg.IssueContext) if cfg.Prompt != "" { - if cfg.Role == RoleWorker && issueContext != "" { + if cfg.Role == sessionPromptRoleWorker && issueContext != "" { return strings.TrimRight(cfg.Prompt, "\n") + "\n\n" + issueContextSection(issueContext) } return cfg.Prompt @@ -63,7 +54,7 @@ func BuildTaskPrompt(cfg TaskConfig) string { if cfg.IssueID == "" { return "" } - if cfg.Role == RoleWorker && issueContext != "" { + if cfg.Role == sessionPromptRoleWorker && issueContext != "" { return fmt.Sprintf(`Work on issue %s. Use the issue context below as task context. First inspect the relevant code and tests, then implement the smallest appropriate fix. Run focused verification. When complete, push the branch and open or update a PR if this project uses PRs. @@ -73,16 +64,15 @@ Use the issue context below as task context. First inspect the relevant code and return fmt.Sprintf("Work on issue %s.\n\nIssue details were not pre-fetched. Start by reading the issue from the tracker, then inspect the relevant code and tests. Implement the smallest appropriate fix, run focused verification, and open or update a PR if this project uses PRs.", cfg.IssueID) } -// BuildSystemPrompt builds the standing system prompt for a session role. -func BuildSystemPrompt(cfg SystemConfig) string { +func buildSystemPromptText(cfg systemPromptConfig) string { sections := make([]string, 0, 5) switch cfg.Role { - case RoleOrchestrator: + case sessionPromptRoleOrchestrator: sections = append(sections, orchestratorSystemPrompt(cfg.Project)) if rules := strings.TrimSpace(cfg.OrchestratorRules); rules != "" { sections = append(sections, "## Project-Specific Orchestrator Rules\n"+rules) } - case RoleWorker: + case sessionPromptRoleWorker: sections = append(sections, workerSystemPrompt(cfg.Project)) if orchestratorID := strings.TrimSpace(cfg.OrchestratorSessionID); orchestratorID != "" { sections = append(sections, workerOrchestratorPrompt(orchestratorID)) @@ -97,16 +87,16 @@ func BuildSystemPrompt(cfg SystemConfig) string { return strings.Join(sections, "\n\n") + systemPromptGuard } -// BuildProjectRules loads worker rules from inline config and a repo-relative +// buildProjectRules loads worker rules from inline config and a repo-relative // rules file. Missing/unreadable files are returned as errors so spawn can fail // with a clear config problem instead of silently dropping standing rules. -func BuildProjectRules(cfg RulesConfig) (string, error) { +func buildProjectRules(cfg projectRulesConfig) (string, error) { parts := make([]string, 0, 2) if rules := strings.TrimSpace(cfg.AgentRules); rules != "" { parts = append(parts, rules) } if rel := strings.TrimSpace(cfg.AgentRulesFile); rel != "" { - path, err := ProjectRelativeFile(cfg.ProjectPath, rel) + path, err := projectRelativeFile(cfg.ProjectPath, rel) if err != nil { return "", fmt.Errorf("agentRulesFile: %w", err) } @@ -121,9 +111,7 @@ func BuildProjectRules(cfg RulesConfig) (string, error) { return strings.Join(parts, "\n\n"), nil } -// ProjectRelativeFile resolves a repo-relative prompt/rules file and refuses -// paths that escape the project root. -func ProjectRelativeFile(projectPath, rel string) (string, error) { +func projectRelativeFile(projectPath, rel string) (string, error) { if strings.TrimSpace(projectPath) == "" { return "", fmt.Errorf("project path is required") } @@ -147,7 +135,7 @@ func issueContextSection(issueContext string) string { return "## Issue Context\n\n" + issueContext } -func orchestratorSystemPrompt(project ProjectContext) string { +func orchestratorSystemPrompt(project promptProject) string { return fmt.Sprintf(`## AO Orchestrator Role You are the human-facing orchestrator for project %s. @@ -193,7 +181,7 @@ Your job is to coordinate work, not to perform implementation. Keep the project %s`, projectName(project), project.ID, project.ID, project.ID, projectContextSection(project)) } -func workerSystemPrompt(project ProjectContext) string { +func workerSystemPrompt(project promptProject) string { repoRules := `## Git and PR Rules - Work on a feature branch, not the default branch. @@ -254,7 +242,7 @@ AO attributes PRs to this session when the source branch is this session branch Keep branch names inside this session namespace so AO can track every PR you open.` } -func projectContextSection(project ProjectContext) string { +func projectContextSection(project promptProject) string { return fmt.Sprintf(`## Project Context - Project: %s @@ -264,7 +252,7 @@ func projectContextSection(project ProjectContext) string { - Path: %s`, project.ID, projectName(project), projectValue(project.Repo), projectValue(project.DefaultBranch), projectValue(project.Path)) } -func projectName(project ProjectContext) string { +func projectName(project promptProject) string { if name := strings.TrimSpace(project.Name); name != "" { return name } diff --git a/backend/internal/sessionprompt/prompts_test.go b/backend/internal/session_manager/prompt_test.go similarity index 85% rename from backend/internal/sessionprompt/prompts_test.go rename to backend/internal/session_manager/prompt_test.go index dcea72bfc..c0de184c5 100644 --- a/backend/internal/sessionprompt/prompts_test.go +++ b/backend/internal/session_manager/prompt_test.go @@ -1,4 +1,4 @@ -package sessionprompt +package sessionmanager import ( "os" @@ -8,8 +8,8 @@ import ( ) func TestBuildTaskPrompt_IssueContextStaysInTaskPrompt(t *testing.T) { - got := BuildTaskPrompt(TaskConfig{ - Role: RoleWorker, + got := buildTaskPrompt(taskPromptConfig{ + Role: sessionPromptRoleWorker, IssueID: "2272", IssueContext: "Title: Enrich prompts\nBody: Include issue context.", }) @@ -26,9 +26,9 @@ func TestBuildTaskPrompt_IssueContextStaysInTaskPrompt(t *testing.T) { } func TestBuildSystemPrompt_WorkerIncludesRulesAndOrchestrator(t *testing.T) { - got := BuildSystemPrompt(SystemConfig{ - Role: RoleWorker, - Project: ProjectContext{ + got := buildSystemPromptText(systemPromptConfig{ + Role: sessionPromptRoleWorker, + Project: promptProject{ ID: "mer", Name: "Mercury", Repo: "https://github.com/acme/mercury", @@ -59,7 +59,7 @@ func TestBuildProjectRules_ReadsInlineAndFileRules(t *testing.T) { if err := os.WriteFile(filepath.Join(dir, "rules.md"), []byte("File rule.\n"), 0o644); err != nil { t.Fatal(err) } - got, err := BuildProjectRules(RulesConfig{ + got, err := buildProjectRules(projectRulesConfig{ ProjectPath: dir, AgentRules: "Inline rule.", AgentRulesFile: "rules.md", @@ -75,7 +75,7 @@ func TestBuildProjectRules_ReadsInlineAndFileRules(t *testing.T) { } func TestProjectRelativeFileRejectsTraversal(t *testing.T) { - if _, err := ProjectRelativeFile(t.TempDir(), "../rules.md"); err == nil { + if _, err := projectRelativeFile(t.TempDir(), "../rules.md"); err == nil { t.Fatal("expected traversal path to be rejected") } } diff --git a/docs/agent/README.md b/docs/agent/README.md index a897a9bba..995ae966a 100644 --- a/docs/agent/README.md +++ b/docs/agent/README.md @@ -137,7 +137,7 @@ AO splits prompt material into two channels before it reaches an adapter: Generated system prompts are not stored as canonical session state. On spawn and restore, the session manager re-derives them from hardcoded prompt text, project config, and current project/session state, then passes `SystemPrompt` and/or `SystemPromptFile` through `LaunchConfig` or `RestoreConfig`. Any prompt file under `AO_DATA_DIR/prompts//system.md` is a launch artifact only. -The hardcoded AO role and task prompt templates live in `backend/internal/sessionprompt`; `backend/internal/session_manager` only gathers project/session state and delegates prompt assembly there. +The hardcoded AO role and task prompt templates live in `backend/internal/session_manager/prompt.go`; the rest of `backend/internal/session_manager` gathers project/session state and delegates prompt assembly there. Adapters should map `SystemPrompt`/`SystemPromptFile` to the strongest native system/developer-instruction mechanism they support. Adapter-specific compatibility gaps are handled inside adapters; the session manager owns composition, not per-agent flag details.