diff --git a/packages/app/src/i18n/resources/ar.ts b/packages/app/src/i18n/resources/ar.ts index 5225d3f7d..cdc1447bc 100644 --- a/packages/app/src/i18n/resources/ar.ts +++ b/packages/app/src/i18n/resources/ar.ts @@ -1846,8 +1846,6 @@ export const ar: TranslationResources = { metadata: { title: "توليد البيانات الوصفية", info: "تعليمات خاصة بالمشروع يتم إدخالها في الذكاء الاصطناعي الذي يستخدمه Paseo لإنشاء بيانات التعريف - استخدمها لفرض اصطلاحات فريقك مثل تسمية الفرع أو نمط الالتزام أو تنسيق PR", - agentTitle: "عناوين Agent", - agentTitlePlaceholder: "اجعل العناوين ضرورية وأقل من 40 حرفًا", branchName: "اسماء الفروع", branchNamePlaceholder: "بادئة الفروع بـ fet/ أو Fix/, mb/ للفروع الشخصية", commitMessage: "ارتكاب الرسائل", diff --git a/packages/app/src/i18n/resources/en.ts b/packages/app/src/i18n/resources/en.ts index 4694e66dd..1d8f0f01d 100644 --- a/packages/app/src/i18n/resources/en.ts +++ b/packages/app/src/i18n/resources/en.ts @@ -1854,8 +1854,6 @@ export const en = { metadata: { title: "Metadata generation", info: "Project-specific instructions injected into the AI prompts Paseo uses to generate metadata - use them to enforce your team's conventions like branch naming, commit style, or PR format", - agentTitle: "Agent titles", - agentTitlePlaceholder: "Keep titles imperative and under 40 characters", branchName: "Branch names", branchNamePlaceholder: "Prefix branches with feat/ or fix/, mb/ for personal branches", commitMessage: "Commit messages", diff --git a/packages/app/src/i18n/resources/es.ts b/packages/app/src/i18n/resources/es.ts index d27a5c13a..7ac3154e6 100644 --- a/packages/app/src/i18n/resources/es.ts +++ b/packages/app/src/i18n/resources/es.ts @@ -1887,8 +1887,6 @@ export const es: TranslationResources = { metadata: { title: "Generación de metadatos", info: "Instrucciones específicas del proyecto inyectadas en los mensajes de IA quePaseoutiliza para generar metadatos; úselas para hacer cumplir las convenciones de su equipo, como la denominación de ramas, el estilo de confirmación o el formatoPR.", - agentTitle: "TítulosAgent", - agentTitlePlaceholder: "Mantenga los títulos imperativos y de menos de 40 caracteres", branchName: "Nombres de sucursales", branchNamePlaceholder: "Prefijo ramas con feat/ o fix/, mb/ para ramas personales", commitMessage: "Confirmar mensajes", diff --git a/packages/app/src/i18n/resources/fr.ts b/packages/app/src/i18n/resources/fr.ts index bbd52f085..fdf895ce8 100644 --- a/packages/app/src/i18n/resources/fr.ts +++ b/packages/app/src/i18n/resources/fr.ts @@ -1893,8 +1893,6 @@ export const fr: TranslationResources = { metadata: { title: "Génération de métadonnées", info: "Instructions spécifiques au projet injectées dans les invites de l'IA quePaseoutilise pour générer des métadonnées: utilisez-les pour appliquer les conventions de votre équipe telles que la dénomination des branches, le style de validation ou le formatPR.", - agentTitle: "TitresAgent", - agentTitlePlaceholder: "Gardez les titres impératifs et inférieurs à 40 caractères", branchName: "Noms des succursales", branchNamePlaceholder: "Préfixez les branches avec feat/ ou fix/, mb/ pour les branches personnelles", diff --git a/packages/app/src/i18n/resources/ru.ts b/packages/app/src/i18n/resources/ru.ts index 367f29755..8f2ad0054 100644 --- a/packages/app/src/i18n/resources/ru.ts +++ b/packages/app/src/i18n/resources/ru.ts @@ -1879,8 +1879,6 @@ export const ru: TranslationResources = { metadata: { title: "Генерация метаданных", info: "Инструкции для конкретного проекта, внедренные в подсказки ИИ, которые Paseo использует для генерации метаданных. Используйте их для обеспечения соблюдения соглашений вашей команды, таких как наименование ветвей, стиль фиксации или формат PR.", - agentTitle: "Agent заголовки", - agentTitlePlaceholder: "Сохраняйте заголовки обязательными и длиной не более 40 символов.", branchName: "Названия ветвей", branchNamePlaceholder: "Префиксные ветки с feat/ или fix/, mb/ для личных веток", commitMessage: "Фиксировать сообщения", diff --git a/packages/app/src/i18n/resources/zh-CN.ts b/packages/app/src/i18n/resources/zh-CN.ts index 5162ace51..c650185d6 100644 --- a/packages/app/src/i18n/resources/zh-CN.ts +++ b/packages/app/src/i18n/resources/zh-CN.ts @@ -1823,8 +1823,6 @@ export const zhCN: TranslationResources = { metadata: { title: "元数据生成", info: "注入到 Paseo 用来生成元数据的 AI prompts 中的 Project 专属指令,可用于强制执行团队约定,例如分支命名、提交风格或 PR 格式", - agentTitle: "Agent 标题", - agentTitlePlaceholder: "标题保持祈使句且不超过 40 个字符", branchName: "分支名称", branchNamePlaceholder: "分支以 feat/ 或 fix/ 开头,个人分支使用 mb/", commitMessage: "提交消息", diff --git a/packages/app/src/screens/project-settings-screen.tsx b/packages/app/src/screens/project-settings-screen.tsx index b55ed605c..f588ebd31 100644 --- a/packages/app/src/screens/project-settings-screen.tsx +++ b/packages/app/src/screens/project-settings-screen.tsx @@ -58,12 +58,6 @@ interface MetadataPromptField { } const METADATA_PROMPT_FIELDS: Record = { - agentTitle: { - titleKey: "settings.project.metadata.agentTitle", - placeholderKey: "settings.project.metadata.agentTitlePlaceholder", - sectionTestID: "metadata-prompt-agent-title-section", - inputTestID: "metadata-prompt-agent-title-input", - }, branchName: { titleKey: "settings.project.metadata.branchName", placeholderKey: "settings.project.metadata.branchNamePlaceholder", diff --git a/packages/app/src/utils/project-config-form.test.ts b/packages/app/src/utils/project-config-form.test.ts index 21fe8c6a8..67ca9c427 100644 --- a/packages/app/src/utils/project-config-form.test.ts +++ b/packages/app/src/utils/project-config-form.test.ts @@ -11,7 +11,6 @@ function emptyDraft(): ProjectConfigDraft { teardownOriginalKind: "missing", scripts: [], metadataPrompts: { - agentTitle: "", branchName: "", commitMessage: "", pullRequest: "", @@ -217,17 +216,15 @@ describe("applyDraftToConfig", () => { expect(tunnel.port).toBe("auto"); }); - it("reads metadata prompt instructions for all four keys", () => { + it("reads metadata prompt instructions for visible keys", () => { const draft = configToDraft({ metadataGeneration: { - agentTitle: { instructions: "Use mb/." }, branchName: { instructions: "feat/" }, commitMessage: { instructions: "Conventional commits." }, pullRequest: { instructions: "Include risk notes." }, }, }); expect(draft.metadataPrompts).toEqual({ - agentTitle: "Use mb/.", branchName: "feat/", commitMessage: "Conventional commits.", pullRequest: "Include risk notes.", @@ -236,11 +233,27 @@ describe("applyDraftToConfig", () => { it("defaults metadata prompts to empty strings when not present", () => { const draft = configToDraft({ - metadataGeneration: { agentTitle: { instructions: "Use mb/." } }, + metadataGeneration: { branchName: { instructions: "feat/" } }, }); expect(draft.metadataPrompts).toEqual({ - agentTitle: "Use mb/.", - branchName: "", + branchName: "feat/", + commitMessage: "", + pullRequest: "", + }); + }); + + it("does not expose legacy agentTitle as a metadata prompt", () => { + const draft = configToDraft( + PaseoConfigRawSchema.parse({ + metadataGeneration: { + agentTitle: { instructions: "Use mb/." }, + branchName: { instructions: "feat/" }, + }, + }), + ); + + expect(draft.metadataPrompts).toEqual({ + branchName: "feat/", commitMessage: "", pullRequest: "", }); @@ -249,11 +262,11 @@ describe("applyDraftToConfig", () => { it("writes only metadata prompt entries with non-empty text", () => { const base: PaseoConfigRaw = {}; const draft = configToDraft(base); - draft.metadataPrompts.agentTitle = "Use mb/."; + draft.metadataPrompts.branchName = "Use mb/."; draft.metadataPrompts.commitMessage = "Conventional commits."; const next = applyDraftToConfig({ draft, base }); expect(next.metadataGeneration).toEqual({ - agentTitle: { instructions: "Use mb/." }, + branchName: { instructions: "Use mb/." }, commitMessage: { instructions: "Conventional commits." }, }); }); @@ -261,16 +274,16 @@ describe("applyDraftToConfig", () => { it("drops the metadataGeneration field when all prompts are empty", () => { const base = PaseoConfigRawSchema.parse({ metadataGeneration: { - agentTitle: { instructions: "Use mb/." }, + branchName: { instructions: "Use mb/." }, }, }); const draft = configToDraft(base); - draft.metadataPrompts.agentTitle = ""; + draft.metadataPrompts.branchName = ""; const next = applyDraftToConfig({ draft, base }); expect(next.metadataGeneration).toBeUndefined(); }); - it("preserves unknown sibling fields inside metadataGeneration on round-trip", () => { + it("preserves legacy and unknown sibling fields inside metadataGeneration on round-trip", () => { const base = PaseoConfigRawSchema.parse({ metadataGeneration: { agentTitle: { instructions: "Use mb/." }, @@ -278,37 +291,38 @@ describe("applyDraftToConfig", () => { }, }); const draft = configToDraft(base); - draft.metadataPrompts.agentTitle = "Use prefix mb/ on titles."; + draft.metadataPrompts.branchName = "Use prefix mb/ on branches."; const next = applyDraftToConfig({ draft, base }); const metadata = next.metadataGeneration as Record; - expect(metadata.agentTitle).toEqual({ instructions: "Use prefix mb/ on titles." }); + expect(metadata.agentTitle).toEqual({ instructions: "Use mb/." }); + expect(metadata.branchName).toEqual({ instructions: "Use prefix mb/ on branches." }); expect(metadata.futureField).toBe(42); }); it("preserves unknown fields inside a metadata prompt entry on round-trip", () => { const base = PaseoConfigRawSchema.parse({ metadataGeneration: { - agentTitle: { instructions: "Use mb/.", model: "haiku" }, + branchName: { instructions: "Use mb/.", model: "haiku" }, }, }); const draft = configToDraft(base); - draft.metadataPrompts.agentTitle = "Updated."; + draft.metadataPrompts.branchName = "Updated."; const next = applyDraftToConfig({ draft, base }); const metadata = next.metadataGeneration as Record; - expect(metadata.agentTitle).toEqual({ instructions: "Updated.", model: "haiku" }); + expect(metadata.branchName).toEqual({ instructions: "Updated.", model: "haiku" }); }); it("clears instructions but preserves unknown sibling fields when text becomes empty", () => { const base = PaseoConfigRawSchema.parse({ metadataGeneration: { - agentTitle: { instructions: "Use mb/.", model: "haiku" }, + branchName: { instructions: "Use mb/.", model: "haiku" }, }, }); const draft = configToDraft(base); - draft.metadataPrompts.agentTitle = ""; + draft.metadataPrompts.branchName = ""; const next = applyDraftToConfig({ draft, base }); const metadata = next.metadataGeneration as Record; - expect(metadata.agentTitle).toEqual({ model: "haiku" }); + expect(metadata.branchName).toEqual({ model: "haiku" }); }); it("drops scripts with an empty name and removes scripts no longer present in the draft", () => { diff --git a/packages/app/src/utils/project-config-form.ts b/packages/app/src/utils/project-config-form.ts index 4d9216555..8e75ba0a4 100644 --- a/packages/app/src/utils/project-config-form.ts +++ b/packages/app/src/utils/project-config-form.ts @@ -7,12 +7,7 @@ import type { export type LifecycleOriginalKind = "string" | "array" | "missing"; -export const METADATA_PROMPT_KEYS = [ - "agentTitle", - "branchName", - "commitMessage", - "pullRequest", -] as const; +export const METADATA_PROMPT_KEYS = ["branchName", "commitMessage", "pullRequest"] as const; export type MetadataPromptKey = (typeof METADATA_PROMPT_KEYS)[number]; export interface ProjectScriptDraft { @@ -105,7 +100,6 @@ function nextScriptDraftId(): string { function emptyMetadataPrompts(): Record { return { - agentTitle: "", branchName: "", commitMessage: "", pullRequest: "", diff --git a/packages/protocol/src/agent-title-limits.ts b/packages/protocol/src/agent-title-limits.ts index 05d7a0371..3d56bec70 100644 --- a/packages/protocol/src/agent-title-limits.ts +++ b/packages/protocol/src/agent-title-limits.ts @@ -1,2 +1 @@ export const MAX_EXPLICIT_AGENT_TITLE_CHARS = 200; -export const MAX_AUTO_AGENT_TITLE_CHARS = 40; diff --git a/packages/protocol/src/paseo-config-schema.test.ts b/packages/protocol/src/paseo-config-schema.test.ts index 49d5dd016..4a5f00f62 100644 --- a/packages/protocol/src/paseo-config-schema.test.ts +++ b/packages/protocol/src/paseo-config-schema.test.ts @@ -37,7 +37,6 @@ describe("paseo config schema", () => { expect( PaseoConfigSchema.parse({ metadataGeneration: { - agentTitle: { instructions: "Use concise titles." }, branchName: { instructions: "Prefix branches with feat/." }, commitMessage: { instructions: "Use imperative mood." }, pullRequest: { instructions: "Include risk notes." }, @@ -45,7 +44,6 @@ describe("paseo config schema", () => { }), ).toEqual({ metadataGeneration: { - agentTitle: { instructions: "Use concise titles." }, branchName: { instructions: "Prefix branches with feat/." }, commitMessage: { instructions: "Use imperative mood." }, pullRequest: { instructions: "Include risk notes." }, @@ -56,29 +54,40 @@ describe("paseo config schema", () => { it("parses partial metadata generation instructions with missing entries undefined", () => { const parsed = PaseoConfigSchema.parse({ metadataGeneration: { - agentTitle: { instructions: "Keep it short." }, + branchName: { instructions: "Keep it short." }, }, }); expect(parsed.metadataGeneration).toEqual({ - agentTitle: { instructions: "Keep it short." }, + branchName: { instructions: "Keep it short." }, }); - expect(parsed.metadataGeneration?.branchName).toBeUndefined(); expect(parsed.metadataGeneration?.commitMessage).toBeUndefined(); expect(parsed.metadataGeneration?.pullRequest).toBeUndefined(); }); + it("preserves legacy agentTitle metadata instructions as passthrough", () => { + expect( + PaseoConfigSchema.parse({ + metadataGeneration: { + agentTitle: { instructions: "Use concise titles." }, + }, + }), + ).toEqual({ + metadataGeneration: { + agentTitle: { instructions: "Use concise titles." }, + }, + }); + }); + it("passes through unknown metadata generation fields", () => { expect( PaseoConfigSchema.parse({ metadataGeneration: { - agentTitle: { instructions: "Use concise titles." }, futureField: 42, }, }), ).toEqual({ metadataGeneration: { - agentTitle: { instructions: "Use concise titles." }, futureField: 42, }, }); @@ -88,7 +97,7 @@ describe("paseo config schema", () => { expect( PaseoConfigSchema.parse({ metadataGeneration: { - agentTitle: { + branchName: { instructions: "Use concise titles.", model: "haiku", }, @@ -96,7 +105,7 @@ describe("paseo config schema", () => { }), ).toEqual({ metadataGeneration: { - agentTitle: { + branchName: { instructions: "Use concise titles.", model: "haiku", }, @@ -108,17 +117,17 @@ describe("paseo config schema", () => { expect( PaseoConfigSchema.parse({ metadataGeneration: { - agentTitle: { instructions: 42 }, + branchName: { instructions: 42 }, }, }), ).toEqual({ metadataGeneration: { - agentTitle: {}, + branchName: {}, }, }); }); - it("raw schema preserves old-style config while accepting metadata generation", () => { + it("raw schema preserves old-style config while accepting legacy agentTitle", () => { const config = { worktree: { setup: "npm install", @@ -132,6 +141,7 @@ describe("paseo config schema", () => { }, metadataGeneration: { agentTitle: { instructions: "Use concise titles." }, + branchName: { instructions: "Use concise branches." }, }, }; @@ -142,12 +152,12 @@ describe("paseo config schema", () => { expect( PaseoConfigRawSchema.parse({ metadataGeneration: { - agentTitle: { instructions: 42 }, + branchName: { instructions: 42 }, }, }), ).toEqual({ metadataGeneration: { - agentTitle: {}, + branchName: {}, }, }); }); diff --git a/packages/protocol/src/paseo-config-schema.ts b/packages/protocol/src/paseo-config-schema.ts index 534f24060..587f8bd5d 100644 --- a/packages/protocol/src/paseo-config-schema.ts +++ b/packages/protocol/src/paseo-config-schema.ts @@ -39,11 +39,12 @@ export const PaseoMetadataGenerationEntrySchema = z export const PaseoMetadataGenerationSchema = z .object({ - agentTitle: PaseoMetadataGenerationEntrySchema.optional(), branchName: PaseoMetadataGenerationEntrySchema.optional(), commitMessage: PaseoMetadataGenerationEntrySchema.optional(), pullRequest: PaseoMetadataGenerationEntrySchema.optional(), }) + // COMPAT(projectMetadataAgentTitle): `agentTitle` project metadata prompts were removed + // in v0.1.96; keep legacy paseo.json parseable until 2026-12-16. .passthrough() .catch({}); diff --git a/packages/server/src/server/agent/agent-manager.test.ts b/packages/server/src/server/agent/agent-manager.test.ts index 85b79ab5d..9de65fb7a 100644 --- a/packages/server/src/server/agent/agent-manager.test.ts +++ b/packages/server/src/server/agent/agent-manager.test.ts @@ -1862,86 +1862,6 @@ test("updateAgentMetadata bumps updatedAt for stored agents", async () => { expect(Date.parse(after!.updatedAt)).toBeGreaterThan(Date.parse(before!.updatedAt)); }); -test("setGeneratedTitle persists generated title when no title exists", async () => { - const workdir = mkdtempSync(join(tmpdir(), "agent-manager-generated-title-empty-")); - const storagePath = join(workdir, "agents"); - const storage = new AgentStorage(storagePath, logger); - const manager = new AgentManager({ - clients: { - codex: new TestAgentClient(), - }, - registry: storage, - logger, - idFactory: () => "00000000-0000-4000-8000-000000000129", - }); - - const snapshot = await manager.createAgent({ - provider: "codex", - cwd: workdir, - }); - - await manager.setGeneratedTitle(snapshot.id, "Generated title"); - - const after = await storage.get(snapshot.id); - expect(after?.title).toBe("Generated title"); -}); - -test("setGeneratedTitle ignores blank generated titles", async () => { - const workdir = mkdtempSync(join(tmpdir(), "agent-manager-generated-title-blank-")); - const storagePath = join(workdir, "agents"); - const storage = new AgentStorage(storagePath, logger); - const manager = new AgentManager({ - clients: { - codex: new TestAgentClient(), - }, - registry: storage, - logger, - idFactory: () => "00000000-0000-4000-8000-000000000130", - }); - - const snapshot = await manager.createAgent({ - provider: "codex", - cwd: workdir, - }); - const before = await storage.get(snapshot.id); - expect(before).not.toBeNull(); - - const stateEvents: ManagedAgent[] = []; - manager.subscribe( - (event) => { - if (event.type === "agent_state") { - stateEvents.push(event.agent); - } - }, - { agentId: snapshot.id, replayState: false }, - ); - - await manager.setGeneratedTitle(snapshot.id, " "); - - const after = await storage.get(snapshot.id); - expect(after?.title).toBeNull(); - expect(after?.updatedAt).toBe(before?.updatedAt); - expect(manager.getAgent(snapshot.id)?.updatedAt.toISOString()).toBe(before?.updatedAt); - expect(stateEvents).toEqual([]); -}); - -test("setGeneratedTitle throws for an unknown agent", async () => { - const workdir = mkdtempSync(join(tmpdir(), "agent-manager-generated-title-unknown-")); - const storagePath = join(workdir, "agents"); - const storage = new AgentStorage(storagePath, logger); - const manager = new AgentManager({ - clients: { - codex: new TestAgentClient(), - }, - registry: storage, - logger, - }); - - await expect( - manager.setGeneratedTitle("00000000-0000-4000-8000-000000000999", "Generated title"), - ).rejects.toThrow("Unknown agent '00000000-0000-4000-8000-000000000999'"); -}); - test("persists live mode, model, and thinking changes without an external snapshot subscriber", async () => { const workdir = mkdtempSync(join(tmpdir(), "agent-manager-live-persist-")); const storagePath = join(workdir, "agents"); diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts index 5955c8b92..79d6d4241 100644 --- a/packages/server/src/server/agent/agent-manager.ts +++ b/packages/server/src/server/agent/agent-manager.ts @@ -1331,20 +1331,6 @@ export class AgentManager { this.emitState(agent, { persist: false }); } - async setGeneratedTitle(agentId: string, title: string): Promise { - const agent = this.requireAgent(agentId); - const normalizedTitle = title.trim(); - if (!normalizedTitle) { - return; - } - - const registry = this.requireRegistry(); - const persisted = await registry.setGeneratedTitle(agent.id, normalizedTitle); - - agent.updatedAt = new Date(persisted.updatedAt); - this.emitState(agent, { persist: false }); - } - async setLabels(agentId: string, labels: Record): Promise { const agent = this.requireAgent(agentId); agent.labels = { ...agent.labels, ...labels }; diff --git a/packages/server/src/server/agent/agent-metadata-generator.real.e2e.test.ts b/packages/server/src/server/agent/agent-metadata-generator.real.e2e.test.ts deleted file mode 100644 index 0dd23ad70..000000000 --- a/packages/server/src/server/agent/agent-metadata-generator.real.e2e.test.ts +++ /dev/null @@ -1,234 +0,0 @@ -import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest"; -import { mkdtempSync, readdirSync, rmSync, statSync, realpathSync } from "fs"; -import { homedir, tmpdir } from "os"; -import path from "path"; -import pino from "pino"; -import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"; - -import { AgentManager } from "./agent-manager.js"; -import { AgentStorage } from "./agent-storage.js"; -import { shutdownProviders } from "./provider-registry.js"; -import { generateAndApplyAgentMetadata } from "./agent-metadata-generator.js"; -import { OpenCodeServerManager } from "./providers/opencode/server-manager.js"; -import { - canRunRealProvider, - createRealProviderClients, - getRealProviderConfig, -} from "../daemon-e2e/real-provider-test-config.js"; - -const CODEX_TEST_MODEL = getRealProviderConfig("codex").model; -const CODEX_TEST_THINKING_OPTION_ID = getRealProviderConfig("codex").thinkingOptionId; -const CLAUDE_TEST_MODEL = getRealProviderConfig("claude").model; -const OPENCODE_TEST_MODEL = getRealProviderConfig("opencode").model; - -function collectFilesRecursively(root: string, filter: (name: string) => boolean): Set { - const results = new Set(); - try { - statSync(root); - } catch { - return results; - } - const stack: string[] = [root]; - while (stack.length > 0) { - const dir = stack.pop()!; - let entries: ReturnType>; - try { - entries = readdirSync(dir, { withFileTypes: true }); - } catch { - continue; - } - for (const entry of entries) { - const full = path.join(dir, entry.name); - if (entry.isDirectory()) { - stack.push(full); - } else if (entry.isFile() && filter(entry.name)) { - results.add(full); - } - } - } - return results; -} - -function collectCodexRolloutFiles(): Set { - const codexHome = process.env.CODEX_HOME ?? path.join(homedir(), ".codex"); - return collectFilesRecursively(path.join(codexHome, "sessions"), (name) => - name.startsWith("rollout-"), - ); -} - -function encodeClaudeProjectDir(cwd: string): string { - return cwd.replaceAll("/", "-"); -} - -function collectClaudeProjectFiles(cwd: string): Set { - const dir = path.join(homedir(), ".claude", "projects", encodeClaudeProjectDir(cwd)); - return collectFilesRecursively(dir, (name) => name.endsWith(".jsonl")); -} - -function tmpCwd(prefix: string): string { - return realpathSync(mkdtempSync(path.join(tmpdir(), prefix))); -} - -describe("agent metadata generation (real agents)", () => { - const logger = pino({ level: "silent" }); - let cwd: string; - let paseoHome: string; - let manager: AgentManager; - let storage: AgentStorage; - let codexAvailable = false; - let claudeAvailable = false; - let opencodeAvailable = false; - - beforeAll(async () => { - [codexAvailable, claudeAvailable, opencodeAvailable] = await Promise.all([ - canRunRealProvider("codex"), - canRunRealProvider("claude"), - canRunRealProvider("opencode"), - ]); - }); - - beforeEach(() => { - cwd = tmpCwd("metadata-cwd-"); - paseoHome = tmpCwd("metadata-paseo-home-"); - storage = new AgentStorage(path.join(paseoHome, "agents"), logger); - manager = new AgentManager({ - clients: createRealProviderClients(["codex", "claude", "opencode"], logger), - registry: storage, - logger, - }); - }); - - afterEach(async () => { - await shutdownProviders(logger); - rmSync(cwd, { recursive: true, force: true }); - rmSync(paseoHome, { recursive: true, force: true }); - }, 60000); - - test("generates a title using a real Codex agent without persisting a rollout", async (ctx) => { - if (!codexAvailable) { - ctx.skip(); - } - const agent = await manager.createAgent( - { - provider: "codex", - model: CODEX_TEST_MODEL, - ...(CODEX_TEST_THINKING_OPTION_ID - ? { thinkingOptionId: CODEX_TEST_THINKING_OPTION_ID } - : {}), - modeId: "auto", - cwd: cwd, - title: "Main Agent", - }, - "4e0a4508-e522-4fe9-8384-cf3bf889f16d", - ); - - const rolloutsBefore = collectCodexRolloutFiles(); - - await generateAndApplyAgentMetadata({ - agentManager: manager, - agentId: agent.id, - cwd: cwd, - initialPrompt: "Use the exact title 'Metadata Title E2E'.", - explicitTitle: null, - paseoHome, - logger, - }); - - await storage.flush(); - const record = await storage.get(agent.id); - expect(record?.title).toBe("Metadata Title E2E"); - - const rolloutsAfter = collectCodexRolloutFiles(); - const newRollouts = [...rolloutsAfter].filter((file) => !rolloutsBefore.has(file)); - expect(newRollouts).toEqual([]); - - await manager.closeAgent(agent.id); - }, 180000); - - test("generates a title using a real Claude agent without persisting a session", async (ctx) => { - if (!claudeAvailable) { - ctx.skip(); - } - const agent = await manager.createAgent( - { - provider: "claude", - model: CLAUDE_TEST_MODEL, - thinkingOptionId: "on", - cwd: cwd, - title: "Main Claude Agent", - }, - "5e1b5619-f633-5fea-9495-d04bf990f27e", - ); - - const sessionsBefore = collectClaudeProjectFiles(cwd); - - await generateAndApplyAgentMetadata({ - agentManager: manager, - agentId: agent.id, - cwd: cwd, - initialPrompt: "Use the exact title 'Claude Metadata Title'.", - explicitTitle: null, - paseoHome, - logger, - }); - - await storage.flush(); - const record = await storage.get(agent.id); - expect(record?.title).toBe("Claude Metadata Title"); - - const sessionsAfter = collectClaudeProjectFiles(cwd); - const newSessions = [...sessionsAfter].filter((file) => !sessionsBefore.has(file)); - expect(newSessions).toEqual([]); - - await manager.closeAgent(agent.id); - }, 180000); - - test("generates a title using a real OpenCode agent and deletes the ephemeral session", async (ctx) => { - if (!opencodeAvailable) { - ctx.skip(); - } - const agent = await manager.createAgent( - { - provider: "opencode", - model: OPENCODE_TEST_MODEL, - modeId: "build", - cwd: cwd, - title: "Main OpenCode Agent", - }, - "6e2c6720-e744-6fdb-a5a6-e15cf0a1f380", - ); - - const acquisition = await OpenCodeServerManager.getInstance(logger).acquire({ force: false }); - const inspectClient = createOpencodeClient({ - baseUrl: acquisition.server.url, - directory: cwd, - }); - try { - const sessionsBeforeRes = await inspectClient.session.list({ directory: cwd }); - const sessionIdsBefore = new Set((sessionsBeforeRes.data ?? []).map((session) => session.id)); - - await generateAndApplyAgentMetadata({ - agentManager: manager, - agentId: agent.id, - cwd: cwd, - initialPrompt: "Use the exact title 'OpenCode Metadata Title'.", - explicitTitle: null, - paseoHome, - logger, - }); - - await storage.flush(); - const record = await storage.get(agent.id); - expect(record?.title).toBe("OpenCode Metadata Title"); - - const sessionsAfterRes = await inspectClient.session.list({ directory: cwd }); - const newSessions = (sessionsAfterRes.data ?? []).filter( - (session) => !sessionIdsBefore.has(session.id), - ); - expect(newSessions).toEqual([]); - } finally { - acquisition.release(); - await manager.closeAgent(agent.id); - } - }, 180000); -}); diff --git a/packages/server/src/server/agent/agent-metadata-generator.ts b/packages/server/src/server/agent/agent-metadata-generator.ts deleted file mode 100644 index 35035b3fc..000000000 --- a/packages/server/src/server/agent/agent-metadata-generator.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { z } from "zod"; -import type { Logger } from "pino"; - -import type { AgentManager } from "./agent-manager.js"; -import { - StructuredAgentFallbackError, - StructuredAgentResponseError, - generateStructuredAgentResponseWithFallback, -} from "./agent-response-loop.js"; -import { - resolveStructuredGenerationProviders, - type StructuredGenerationDaemonConfig, -} from "./structured-generation-providers.js"; -import { MAX_AUTO_AGENT_TITLE_CHARS } from "@getpaseo/protocol/agent-title-limits"; -import { buildMetadataPrompt } from "../../utils/build-metadata-prompt.js"; -import type { WorkspaceGitService } from "../workspace-git-service.js"; -import type { ProviderSnapshotManager } from "./provider-snapshot-manager.js"; - -export interface AgentMetadataGeneratorDeps { - generateStructuredAgentResponseWithFallback?: typeof generateStructuredAgentResponseWithFallback; -} - -export interface AgentMetadataGenerationOptions { - agentManager: AgentManager; - agentId: string; - cwd: string; - workspaceGitService?: Pick; - providerSnapshotManager?: Pick; - daemonConfig?: StructuredGenerationDaemonConfig | null; - currentSelection?: { - provider?: string | null; - model?: string | null; - thinkingOptionId?: string | null; - }; - initialPrompt?: string | null; - explicitTitle?: string | null; - paseoHome?: string; - logger: Logger; - deps?: AgentMetadataGeneratorDeps; -} - -interface AgentMetadataNeeds { - prompt: string | null; - needsTitle: boolean; -} - -function hasExplicitTitle(title?: string | null): boolean { - return Boolean(title && title.trim().length > 0); -} - -function normalizeAutoTitle(title: string): string | null { - const normalized = title.trim(); - if (!normalized) { - return null; - } - return normalized.slice(0, MAX_AUTO_AGENT_TITLE_CHARS).trim() || null; -} - -export async function determineAgentMetadataNeeds( - options: Pick< - AgentMetadataGenerationOptions, - "initialPrompt" | "explicitTitle" | "cwd" | "paseoHome" | "deps" - >, -): Promise { - const prompt = options.initialPrompt?.trim(); - if (!prompt) { - return { prompt: null, needsTitle: false }; - } - - const needsTitle = !hasExplicitTitle(options.explicitTitle); - - return { - prompt, - needsTitle, - }; -} - -function buildMetadataSchema( - needs: AgentMetadataNeeds, -): z.ZodObject> | null { - if (!needs.needsTitle) { - return null; - } - - const shape: Record = {}; - if (needs.needsTitle) { - shape.title = z.string().min(1).max(MAX_AUTO_AGENT_TITLE_CHARS); - } - return z.object(shape); -} - -async function buildPrompt( - needs: AgentMetadataNeeds, - options: { - cwd: string; - workspaceGitService?: Pick; - }, -): Promise { - const beforeLines: string[] = ["Generate metadata for a coding agent based on the user prompt."]; - if (needs.needsTitle) { - beforeLines.push(`Title: short descriptive label (<= ${MAX_AUTO_AGENT_TITLE_CHARS} chars).`); - } - - return buildMetadataPrompt({ - cwd: options.cwd, - workspaceGitService: options.workspaceGitService, - configKey: "agentTitle", - before: beforeLines.join("\n"), - after: "Return JSON only with a single field 'title'.", - trailing: `User prompt:\n${needs.prompt ?? ""}`, - }); -} - -export async function generateAndApplyAgentMetadata( - options: AgentMetadataGenerationOptions, -): Promise { - const needs = await determineAgentMetadataNeeds(options); - if (!needs.prompt) { - return; - } - - const schema = buildMetadataSchema(needs); - if (!schema) { - return; - } - - const generator = - options.deps?.generateStructuredAgentResponseWithFallback ?? - generateStructuredAgentResponseWithFallback; - - let result: { title?: string }; - - try { - const providers = options.providerSnapshotManager - ? await resolveStructuredGenerationProviders({ - cwd: options.cwd, - providerSnapshotManager: options.providerSnapshotManager, - daemonConfig: options.daemonConfig, - currentSelection: options.currentSelection, - }) - : []; - result = await generator({ - manager: options.agentManager, - cwd: options.cwd, - prompt: await buildPrompt(needs, { - cwd: options.cwd, - workspaceGitService: options.workspaceGitService, - }), - schema, - schemaName: "AgentMetadata", - maxRetries: 2, - providers, - persistSession: false, - logger: options.logger, - agentConfigOverrides: { - title: "Agent metadata generator", - internal: true, - }, - }); - } catch (error) { - const attempts = error instanceof StructuredAgentFallbackError ? error.attempts : undefined; - options.logger.error( - { err: error, agentId: options.agentId, attempts }, - error instanceof StructuredAgentResponseError || error instanceof StructuredAgentFallbackError - ? "Structured metadata generation failed" - : "Agent metadata generation failed", - ); - return; - } - - if (needs.needsTitle && typeof result.title === "string") { - const normalizedTitle = normalizeAutoTitle(result.title); - if (normalizedTitle) { - await options.agentManager.setGeneratedTitle(options.agentId, normalizedTitle); - } - } -} - -export function scheduleAgentMetadataGeneration(options: AgentMetadataGenerationOptions): void { - queueMicrotask(() => { - void generateAndApplyAgentMetadata(options).catch((error) => { - options.logger.error( - { err: error, agentId: options.agentId }, - "Agent metadata generation crashed", - ); - }); - }); -} diff --git a/packages/server/src/server/agent/agent-metadata-generator.unit.test.ts b/packages/server/src/server/agent/agent-metadata-generator.unit.test.ts deleted file mode 100644 index 46a9a5235..000000000 --- a/packages/server/src/server/agent/agent-metadata-generator.unit.test.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import path from "node:path"; -import { afterEach, describe, expect, it, vi } from "vitest"; - -import { createTestLogger } from "../../test-utils/test-logger.js"; -import { createNoopWorkspaceGitService } from "../test-utils/workspace-git-service-stub.js"; -import { MAX_AUTO_AGENT_TITLE_CHARS } from "@getpaseo/protocol/agent-title-limits"; -import { - generateAndApplyAgentMetadata, - type AgentMetadataGeneratorDeps, -} from "./agent-metadata-generator.js"; -import type { AgentManager } from "./agent-manager.js"; - -const logger = createTestLogger(); -const cleanupPaths: string[] = []; -const PRE_CHANGE_TITLE_PROMPT = `Generate metadata for a coding agent based on the user prompt. -Title: short descriptive label (<= ${MAX_AUTO_AGENT_TITLE_CHARS} chars). -Return JSON only with a single field 'title'. - -User prompt: -Implement this feature`; - -afterEach(() => { - for (const target of cleanupPaths.splice(0)) { - rmSync(target, { recursive: true, force: true }); - } -}); - -function createDeps( - generateStructuredAgentResponseWithFallback: NonNullable< - AgentMetadataGeneratorDeps["generateStructuredAgentResponseWithFallback"] - >, -): AgentMetadataGeneratorDeps { - return { - generateStructuredAgentResponseWithFallback, - }; -} - -describe("agent metadata generator auto-title", () => { - it("caps generated auto titles at 40 characters before persisting", async () => { - const setGeneratedTitle = vi.fn().mockResolvedValue(undefined); - const manager = { setGeneratedTitle } as unknown as AgentManager; - const generatedTitle = "x".repeat(MAX_AUTO_AGENT_TITLE_CHARS + 25); - const generateStructured = vi.fn().mockResolvedValue({ title: generatedTitle }) as NonNullable< - AgentMetadataGeneratorDeps["generateStructuredAgentResponseWithFallback"] - >; - - await generateAndApplyAgentMetadata({ - agentManager: manager, - agentId: "agent-1", - cwd: "/tmp/repo", - initialPrompt: "Implement this feature", - explicitTitle: null, - logger, - deps: createDeps(generateStructured), - }); - - expect(setGeneratedTitle).toHaveBeenCalledTimes(1); - expect(setGeneratedTitle).toHaveBeenCalledWith( - "agent-1", - "x".repeat(MAX_AUTO_AGENT_TITLE_CHARS), - ); - }); - - it("does not generate an auto title when an explicit title is provided", async () => { - const setGeneratedTitle = vi.fn().mockResolvedValue(undefined); - const manager = { setGeneratedTitle } as unknown as AgentManager; - const generateStructured = vi.fn().mockResolvedValue({ title: "Generated" }) as NonNullable< - AgentMetadataGeneratorDeps["generateStructuredAgentResponseWithFallback"] - >; - - await generateAndApplyAgentMetadata({ - agentManager: manager, - agentId: "agent-2", - cwd: "/tmp/repo", - initialPrompt: "Implement this feature", - explicitTitle: "Keep this title", - logger, - deps: createDeps(generateStructured), - }); - - expect(generateStructured).not.toHaveBeenCalled(); - expect(setGeneratedTitle).not.toHaveBeenCalled(); - }); - - it("generates titles independently from workspace branch naming", async () => { - const setGeneratedTitle = vi.fn().mockResolvedValue(undefined); - const manager = { setGeneratedTitle } as unknown as AgentManager; - const generateStructured = vi - .fn() - .mockResolvedValue({ title: "Generated title" }) as NonNullable< - AgentMetadataGeneratorDeps["generateStructuredAgentResponseWithFallback"] - >; - - await generateAndApplyAgentMetadata({ - agentManager: manager, - agentId: "agent-suppressed-branch", - cwd: "/tmp/repo/metadata-worktree", - initialPrompt: "Implement this feature", - explicitTitle: null, - logger, - deps: { - generateStructuredAgentResponseWithFallback: generateStructured, - }, - }); - - expect(generateStructured).toHaveBeenCalledWith( - expect.objectContaining({ - cwd: "/tmp/repo/metadata-worktree", - persistSession: false, - }), - ); - expect(setGeneratedTitle).toHaveBeenCalledWith("agent-suppressed-branch", "Generated title"); - }); - - it.each([ - ["paseo.json missing", undefined], - ["paseo.json exists but invalid JSON", "{ nope"], - ["paseo.json valid but missing metadataGeneration", {}], - [ - "metadataGeneration exists but missing agentTitle", - { metadataGeneration: { branchName: { instructions: "Use mb/." } } }, - ], - ["agentTitle exists but instructions is undefined", { metadataGeneration: { agentTitle: {} } }], - [ - "agentTitle exists but instructions is empty", - { metadataGeneration: { agentTitle: { instructions: "" } } }, - ], - [ - "agentTitle exists but instructions is whitespace-only", - { metadataGeneration: { agentTitle: { instructions: " \n\t " } } }, - ], - ])("keeps the pre-change prompt byte-identical when %s", async (_name, config) => { - const { prompt } = await generateTitlePromptWithConfig(config); - - expect(prompt).toBe(PRE_CHANGE_TITLE_PROMPT); - }); - - it("injects project instructions between the default rules and JSON contract", async () => { - const { prompt } = await generateTitlePromptWithConfig({ - metadataGeneration: { - agentTitle: { - instructions: "Use the prefix mb/.", - }, - }, - }); - - const defaultRuleIndex = prompt.indexOf("Title: short descriptive label"); - const noticeIndex = prompt.indexOf("override the guidelines above"); - const openTagIndex = prompt.indexOf(""); - const userInstructionIndex = prompt.indexOf("Use the prefix mb/."); - const closeTagIndex = prompt.indexOf(""); - const jsonContractIndex = prompt.indexOf("Return JSON only"); - const payloadIndex = prompt.indexOf("User prompt:"); - - expect(defaultRuleIndex).toBeGreaterThanOrEqual(0); - expect(defaultRuleIndex).toBeLessThan(openTagIndex); - expect(openTagIndex).toBeLessThan(noticeIndex); - expect(noticeIndex).toBeLessThan(userInstructionIndex); - expect(userInstructionIndex).toBeLessThan(closeTagIndex); - expect(closeTagIndex).toBeLessThan(jsonContractIndex); - expect(jsonContractIndex).toBeLessThan(payloadIndex); - }); -}); - -async function generateTitlePromptWithConfig(config: unknown): Promise<{ prompt: string }> { - const repoRoot = mkdtempSync(path.join(tmpdir(), "paseo-title-config-")); - cleanupPaths.push(repoRoot); - if (typeof config === "string") { - writeFileSync(path.join(repoRoot, "paseo.json"), config); - } else if (config !== undefined) { - writeFileSync(path.join(repoRoot, "paseo.json"), `${JSON.stringify(config)}\n`); - } - - const setGeneratedTitle = vi.fn().mockResolvedValue(undefined); - const manager = { setGeneratedTitle } as unknown as AgentManager; - const generateStructured = vi.fn().mockResolvedValue({ title: "Generated title" }) as NonNullable< - AgentMetadataGeneratorDeps["generateStructuredAgentResponseWithFallback"] - >; - - await generateAndApplyAgentMetadata({ - agentManager: manager, - agentId: "agent-config", - cwd: path.join(repoRoot, "nested"), - workspaceGitService: createNoopWorkspaceGitService({ - resolveRepoRoot: async () => repoRoot, - }), - initialPrompt: "Implement this feature", - explicitTitle: null, - logger, - deps: createDeps(generateStructured), - }); - - return { - prompt: String(generateStructured.mock.calls[0]?.[0].prompt), - }; -} diff --git a/packages/server/src/server/agent/agent-storage.test.ts b/packages/server/src/server/agent/agent-storage.test.ts index a70f4b3b2..a9b1ab762 100644 --- a/packages/server/src/server/agent/agent-storage.test.ts +++ b/packages/server/src/server/agent/agent-storage.test.ts @@ -320,30 +320,6 @@ describe("AgentStorage", () => { ); }); - test("setGeneratedTitle with concurrent writes does not corrupt state", async () => { - const agentId = "agent-generated-title-concurrent"; - await storage.applySnapshot(createManagedAgent({ id: agentId })); - - await Promise.all([ - storage.setGeneratedTitle(agentId, "Title A"), - storage.setGeneratedTitle(agentId, "Title B"), - ]); - - const record = await storage.get(agentId); - expect(["Title A", "Title B"]).toContain(record?.title); - }); - - test("setGeneratedTitle writes the generated title", async () => { - const agentId = "agent-generated-title-empty"; - await storage.applySnapshot(createManagedAgent({ id: agentId })); - - const written = await storage.setGeneratedTitle(agentId, "Generated title"); - - expect(written.title).toBe("Generated title"); - const record = await storage.get(agentId); - expect(record?.title).toBe("Generated title"); - }); - test("applySnapshot accepts explicit title overrides", async () => { const agentId = "agent-override"; await storage.applySnapshot(createManagedAgent({ id: agentId }), { title: "Provided Title" }); diff --git a/packages/server/src/server/agent/agent-storage.ts b/packages/server/src/server/agent/agent-storage.ts index 99d4528c1..5e00462e4 100644 --- a/packages/server/src/server/agent/agent-storage.ts +++ b/packages/server/src/server/agent/agent-storage.ts @@ -226,22 +226,6 @@ export class AgentStorage { await this.upsert({ ...record, title }); } - async setGeneratedTitle(agentId: string, title: string): Promise { - await this.load(); - await this.waitForPendingWrite(agentId); - const record = this.cache.get(agentId) ?? null; - if (!record) { - throw new Error(`Agent ${agentId} not found`); - } - const nextRecord = { - ...record, - title, - updatedAt: new Date().toISOString(), - }; - await this.queueRecordWrite(nextRecord); - return nextRecord; - } - async flush(): Promise { await this.load().catch(() => undefined); const writes = Array.from(this.pendingWrites.values()); diff --git a/packages/server/src/server/agent/create-agent/create.test.ts b/packages/server/src/server/agent/create-agent/create.test.ts index fb7b0ff5d..0e3037596 100644 --- a/packages/server/src/server/agent/create-agent/create.test.ts +++ b/packages/server/src/server/agent/create-agent/create.test.ts @@ -70,7 +70,6 @@ test("session create forwards clientMessageId to the initial prompt run options" clientMessageId: "msg-create-1", labels: {}, provisionalTitle: null, - explicitTitle: "Explicit title", firstAgentContext: { attachments: [] }, buildSessionConfig: async (config) => ({ sessionConfig: config }), }); @@ -99,7 +98,6 @@ test("session create stamps the requested workspaceId when no worktree setup run workspaceId: "ws-source", labels: {}, provisionalTitle: null, - explicitTitle: null, firstAgentContext: { attachments: [] }, buildSessionConfig: async (config) => ({ sessionConfig: config }), }, @@ -131,7 +129,6 @@ test("session create stamps the new worktree's workspaceId when a setup continua workspaceId: "ws-source", labels: {}, provisionalTitle: null, - explicitTitle: null, firstAgentContext: { attachments: [] }, buildSessionConfig: async (config) => ({ sessionConfig: config, @@ -163,7 +160,6 @@ test("mcp create stamps the new worktree's workspaceId, not the parent's", async workspaceId: "ws-parent", labels: {}, provisionalTitle: null, - explicitTitle: null, firstAgentContext: { attachments: [] }, buildSessionConfig: async (config) => ({ sessionConfig: config }), }, @@ -198,3 +194,77 @@ test("mcp create stamps the new worktree's workspaceId, not the parent's", async rmSync(workdir, { recursive: true, force: true }); } }); + +test("session create keeps the prompt title after the initial prompt settles", async () => { + const workdir = mkdtempSync(join(tmpdir(), "create-agent-title-test-")); + const storage = new AgentStorage(join(workdir, "agents"), logger); + const agentManager = createRealAgentManager(storage); + const title = "Implement auth retries with backoff"; + + try { + const { snapshot } = await createAgentCommand( + { + agentManager, + agentStorage: storage, + logger, + providerSnapshotManager: createProviderSnapshotManagerStub().manager, + }, + { + kind: "session", + config: { provider: "codex", cwd: workdir }, + initialPrompt: `${title}\n\ninclude tests`, + labels: {}, + provisionalTitle: title, + firstAgentContext: { attachments: [] }, + buildSessionConfig: async (config) => ({ sessionConfig: config }), + }, + ); + + const created = await storage.get(snapshot.id); + expect(created?.title).toBe(title); + + await agentManager.waitForAgentEvent(snapshot.id, { waitForActive: true }); + + const settled = await storage.get(snapshot.id); + expect(settled?.title).toBe(title); + } finally { + rmSync(workdir, { recursive: true, force: true }); + } +}); + +test("session create keeps an explicit title after the initial prompt settles", async () => { + const workdir = mkdtempSync(join(tmpdir(), "create-agent-explicit-title-test-")); + const storage = new AgentStorage(join(workdir, "agents"), logger); + const agentManager = createRealAgentManager(storage); + const title = "Explicit override"; + + try { + const { snapshot } = await createAgentCommand( + { + agentManager, + agentStorage: storage, + logger, + providerSnapshotManager: createProviderSnapshotManagerStub().manager, + }, + { + kind: "session", + config: { provider: "codex", cwd: workdir, title }, + initialPrompt: "Implement auth retries with backoff", + labels: {}, + provisionalTitle: title, + firstAgentContext: { attachments: [] }, + buildSessionConfig: async (config) => ({ sessionConfig: config }), + }, + ); + + const created = await storage.get(snapshot.id); + expect(created?.title).toBe(title); + + await agentManager.waitForAgentEvent(snapshot.id, { waitForActive: true }); + + const settled = await storage.get(snapshot.id); + expect(settled?.title).toBe(title); + } finally { + rmSync(workdir, { recursive: true, force: true }); + } +}); diff --git a/packages/server/src/server/agent/create-agent/create.ts b/packages/server/src/server/agent/create-agent/create.ts index a84c68488..e2d38ed0a 100644 --- a/packages/server/src/server/agent/create-agent/create.ts +++ b/packages/server/src/server/agent/create-agent/create.ts @@ -5,7 +5,6 @@ import type { TerminalManager } from "../../../terminal/terminal-manager.js"; import type { CreatePaseoWorktreeInput } from "../../paseo-worktree-service.js"; import { expandUserPath, resolvePathFromBase } from "../../path-utils.js"; import { toWorktreeRequestError } from "../../worktree-errors.js"; -import type { WorkspaceGitService } from "../../workspace-git-service.js"; import type { AgentWorktreeSetupContinuation, CreatePaseoWorktreeSetupContinuationInput, @@ -14,8 +13,6 @@ import type { } from "../../worktree-session.js"; import type { AgentAttachment, FirstAgentContext, GitSetupOptions } from "../../messages.js"; import type { AgentManager, ManagedAgent } from "../agent-manager.js"; -import { scheduleAgentMetadataGeneration } from "../agent-metadata-generator.js"; -import type { StructuredGenerationDaemonConfig } from "../structured-generation-providers.js"; import type { AgentPromptContentBlock, AgentPromptInput, @@ -46,13 +43,8 @@ interface CreateAgentCommandDependencies { logger: Logger; paseoHome?: string; worktreesRoot?: string; - workspaceGitService?: Pick< - WorkspaceGitService, - "getSnapshot" | "listWorktrees" | "resolveRepoRoot" - >; terminalManager?: TerminalManager | null; providerSnapshotManager: ProviderSnapshotManager; - daemonConfig?: StructuredGenerationDaemonConfig | null; createPaseoWorktree?: CreatePaseoWorktreeWorkflowFn; // Mints a fresh workspace for a cwd and returns its id. Used when an agent is // created with no parent and no worktree: it owns a brand-new workspace rather @@ -74,7 +66,6 @@ export interface CreateAgentFromSessionInput { labels: Record; env?: Record; provisionalTitle: string | null; - explicitTitle: string | null; firstAgentContext: FirstAgentContext; buildSessionConfig: ( config: AgentSessionConfig, @@ -124,10 +115,8 @@ export interface CreateAgentCommandResult { interface ResolvedCreateAgent { config: AgentSessionConfig; createOptions?: AgentCreateOptions; - metadataInitialPrompt?: string; prompt?: AgentPromptInput; runOptions?: AgentRunOptions; - explicitTitle: string | null; setupContinuation?: AgentWorktreeSetupContinuation; background: boolean; promptFailure: "throw" | "log"; @@ -221,10 +210,8 @@ async function resolveSessionCreateAgent( // path). createdWorkspaceId is the freshly created worktree's workspace. workspaceId: setupContinuation ? createdWorkspaceId : input.workspaceId, }, - metadataInitialPrompt: trimmedPrompt, prompt: hasPromptContent ? prompt : undefined, runOptions, - explicitTitle: input.explicitTitle, setupContinuation, background: true, promptFailure: "throw", @@ -303,9 +290,7 @@ async function resolveMcpCreateAgent( ...(workspaceId ? { workspaceId } : {}), } : undefined, - metadataInitialPrompt: trimmedPrompt, prompt: trimmedPrompt, - explicitTitle: input.title.trim(), setupContinuation, background: input.background, promptFailure: "log", @@ -327,25 +312,6 @@ async function sendInitialPrompt( resolved: ResolvedCreateAgent, snapshot: ManagedAgent, ): Promise<{ started: boolean; liveSnapshot: ManagedAgent }> { - scheduleAgentMetadataGeneration({ - agentManager: dependencies.agentManager, - agentId: snapshot.id, - cwd: snapshot.cwd, - workspaceGitService: dependencies.workspaceGitService, - providerSnapshotManager: dependencies.providerSnapshotManager, - daemonConfig: dependencies.daemonConfig, - currentSelection: { - provider: snapshot.provider, - model: snapshot.runtimeInfo?.model ?? resolved.config.model, - thinkingOptionId: - snapshot.runtimeInfo?.thinkingOptionId ?? resolved.config.thinkingOptionId ?? null, - }, - initialPrompt: resolved.metadataInitialPrompt, - explicitTitle: resolved.explicitTitle, - paseoHome: dependencies.paseoHome, - logger: dependencies.logger, - }); - try { const prompt = resolved.prompt; if (prompt === undefined) { diff --git a/packages/server/src/server/agent/import-sessions.test.ts b/packages/server/src/server/agent/import-sessions.test.ts index d385e6786..470b36430 100644 --- a/packages/server/src/server/agent/import-sessions.test.ts +++ b/packages/server/src/server/agent/import-sessions.test.ts @@ -379,7 +379,6 @@ test("importProviderSession imports a selected provider session without listing" list: vi.fn().mockResolvedValue([]), get: vi.fn().mockResolvedValue(null), } as unknown as AgentStorage; - const scheduleAgentMetadataGeneration = vi.fn(); const result = await importProviderSession({ request: { @@ -392,7 +391,6 @@ test("importProviderSession imports a selected provider session without listing" agentManager, agentStorage, logger: { warn: vi.fn(), error: vi.fn() } as never, - deps: { scheduleAgentMetadataGeneration }, }); expect(agentManager.importProviderSession).toHaveBeenCalledWith({ @@ -402,15 +400,6 @@ test("importProviderSession imports a selected provider session without listing" workspaceId: "ws-imported", labels: undefined, }); - expect(scheduleAgentMetadataGeneration).toHaveBeenCalledWith( - expect.objectContaining({ - agentManager, - agentId: snapshot.id, - cwd, - initialPrompt: "Trace recent provider sessions\n\nkeep it tight", - explicitTitle: null, - }), - ); expect(result).toEqual({ snapshot, timelineSize: 2 }); }); diff --git a/packages/server/src/server/agent/import-sessions.ts b/packages/server/src/server/agent/import-sessions.ts index ae482f6ea..f2c5b563e 100644 --- a/packages/server/src/server/agent/import-sessions.ts +++ b/packages/server/src/server/agent/import-sessions.ts @@ -7,14 +7,7 @@ import type { ManagedImportableProviderSession, } from "./agent-manager.js"; import type { AgentStorage, StoredAgentRecord } from "./agent-storage.js"; -import type { - AgentPersistenceHandle, - AgentProvider, - AgentTimelineItem, -} from "./agent-sdk-types.js"; -import { scheduleAgentMetadataGeneration } from "./agent-metadata-generator.js"; -import type { StructuredGenerationDaemonConfig } from "./structured-generation-providers.js"; -import { resolveCreateAgentTitles } from "./create-agent-title.js"; +import type { AgentPersistenceHandle, AgentProvider } from "./agent-sdk-types.js"; import { unarchiveAgentState } from "./agent-prompt.js"; import { toRecentProviderSessionDescriptorPayload } from "./agent-projections.js"; import type { @@ -22,7 +15,6 @@ import type { ImportAgentRequestMessageSchema, RecentProviderSessionDescriptorPayload, } from "@getpaseo/protocol/messages"; -import type { WorkspaceGitService } from "../workspace-git-service.js"; import { createRealpathAwarePathMatcher } from "../../utils/path.js"; type ImportAgentRequestMessage = z.infer; @@ -65,14 +57,7 @@ export interface ImportProviderSessionInput { workspaceId: string; agentManager: AgentManager; agentStorage: AgentStorage; - workspaceGitService?: Pick; - providerSnapshotManager?: Pick; - daemonConfig?: StructuredGenerationDaemonConfig | null; - paseoHome?: string; logger: Logger; - deps?: { - scheduleAgentMetadataGeneration?: typeof scheduleAgentMetadataGeneration; - }; } export interface ImportProviderSessionResult { @@ -168,17 +153,6 @@ export async function importProviderSession( labels, }); await unarchiveAgentState(input.agentStorage, input.agentManager, snapshot.id); - scheduleImportedAgentMetadata({ - snapshot, - agentManager: input.agentManager, - workspaceGitService: input.workspaceGitService, - providerSnapshotManager: input.providerSnapshotManager, - daemonConfig: input.daemonConfig, - paseoHome: input.paseoHome, - logger: input.logger, - scheduleAgentMetadataGeneration: - input.deps?.scheduleAgentMetadataGeneration ?? scheduleAgentMetadataGeneration, - }); return { snapshot, @@ -204,48 +178,6 @@ async function unarchiveAgentByHandle( await unarchiveAgentState(agentStorage, agentManager, matched.id); } -function scheduleImportedAgentMetadata(input: { - snapshot: ManagedAgent; - agentManager: AgentManager; - workspaceGitService?: Pick; - providerSnapshotManager?: Pick; - daemonConfig?: StructuredGenerationDaemonConfig | null; - paseoHome?: string; - logger: Logger; - scheduleAgentMetadataGeneration: typeof scheduleAgentMetadataGeneration; -}): void { - const initialPrompt = getFirstUserMessageText(input.agentManager.getTimeline(input.snapshot.id)); - if (!initialPrompt) { - return; - } - - const { explicitTitle } = resolveCreateAgentTitles({ - configTitle: input.snapshot.config.title, - initialPrompt, - }); - - input.scheduleAgentMetadataGeneration({ - agentManager: input.agentManager, - agentId: input.snapshot.id, - cwd: input.snapshot.cwd, - workspaceGitService: input.workspaceGitService, - providerSnapshotManager: input.providerSnapshotManager, - daemonConfig: input.daemonConfig, - currentSelection: { - provider: input.snapshot.provider, - model: input.snapshot.runtimeInfo?.model ?? input.snapshot.config.model, - thinkingOptionId: - input.snapshot.runtimeInfo?.thinkingOptionId ?? - input.snapshot.config.thinkingOptionId ?? - null, - }, - initialPrompt, - explicitTitle, - paseoHome: input.paseoHome, - logger: input.logger, - }); -} - function parseRecentProviderSessionsSince(since: string | undefined): number | null { if (!since) { return null; @@ -273,19 +205,6 @@ function buildImportPersistenceHandle(input: { }; } -function getFirstUserMessageText(timeline: readonly AgentTimelineItem[]): string | null { - for (const item of timeline) { - if (item.type !== "user_message") { - continue; - } - const text = item.text.trim(); - if (text) { - return text; - } - } - return null; -} - async function collectImportedProviderSessionHandles( agentManager: Pick, agentStorage: Pick, diff --git a/packages/server/src/server/agent/mcp-server.ts b/packages/server/src/server/agent/mcp-server.ts index bf3e5ad59..37d7e358b 100644 --- a/packages/server/src/server/agent/mcp-server.ts +++ b/packages/server/src/server/agent/mcp-server.ts @@ -944,7 +944,6 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom logger: childLogger, paseoHome: options.paseoHome, worktreesRoot: options.worktreesRoot, - workspaceGitService: options.workspaceGitService, terminalManager, providerSnapshotManager, createPaseoWorktree: options.createPaseoWorktree, diff --git a/packages/server/src/server/agent/providers/mock-load-test-agent.test.ts b/packages/server/src/server/agent/providers/mock-load-test-agent.test.ts index 1f314c921..914944093 100644 --- a/packages/server/src/server/agent/providers/mock-load-test-agent.test.ts +++ b/packages/server/src/server/agent/providers/mock-load-test-agent.test.ts @@ -74,34 +74,6 @@ describe("MockLoadTestAgentClient", () => { }); }); - test("returns schema-shaped JSON for structured agent-title generation", async () => { - vi.useFakeTimers(); - const client = new MockLoadTestAgentClient(); - const session = await client.createSession({ - provider: "mock", - cwd: process.cwd(), - model: "ten-second-stream", - }); - - const resultPromise = session.run( - [ - "Generate metadata for a coding agent based on the user prompt.", - "Title: short descriptive label (<= 80 chars).", - "Return JSON only with a single field 'title'.", - "", - "User prompt:", - "Fix login bug", - ].join("\n"), - ); - await vi.advanceTimersByTimeAsync(0); - - await expect(resultPromise).resolves.toMatchObject({ - sessionId: session.id, - finalText: JSON.stringify({ title: "Fix login bug" }), - canceled: false, - }); - }); - test("emits sub-word tokens, reasoning, and sequential tool calls during a foreground turn", async () => { vi.useFakeTimers(); const client = new MockLoadTestAgentClient(); diff --git a/packages/server/src/server/agent/providers/mock-load-test-agent.ts b/packages/server/src/server/agent/providers/mock-load-test-agent.ts index 1dfaf8cfd..8b23b9754 100644 --- a/packages/server/src/server/agent/providers/mock-load-test-agent.ts +++ b/packages/server/src/server/agent/providers/mock-load-test-agent.ts @@ -311,37 +311,6 @@ function parseStructuredBranchNamePrompt( return { title: title || "Mock task", branch }; } -function parseStructuredAgentTitlePrompt(prompt: AgentPromptInput): { title: string } | null { - const text = promptToText(prompt); - const hasAgentTitlePrompt = - text.includes("Generate metadata for a coding agent based on the user prompt.") && - (text.includes("Return JSON only with a single field 'title'.") || text.includes('"title"')); - if ( - !hasAgentTitlePrompt && - !( - text.includes("You must respond with JSON only that matches this JSON Schema") && - text.includes('"title"') && - text.includes("User prompt:") - ) - ) { - return null; - } - - const seed = text.split("User prompt:\n").at(-1)?.trim() ?? ""; - const firstLine = - seed - .split("\n") - .find((line) => line.trim().length > 0) - ?.trim() ?? "Mock task"; - const title = firstLine - .replace(/^["'`]+|["'`]+$/g, "") - .replace(/\s+/g, " ") - .slice(0, 80) - .trim(); - - return { title: title || "Mock task" }; -} - function buildRepeatedPayload(bytes: number, prefix: string): string { const line = `${prefix} ${"x".repeat(96)}\n`; let output = ""; @@ -678,11 +647,8 @@ export class MockLoadTestAgentSession implements AgentSession { const stress = parseAgentStreamStressPrompt(prompt); const questionPrompt = parseMockQuestionPrompt(prompt); const structuredBranchName = parseStructuredBranchNamePrompt(prompt); - const structuredAgentTitle = parseStructuredAgentTitlePrompt(prompt); if (structuredBranchName) { this.scheduleStructuredJsonTurn(turn, structuredBranchName); - } else if (structuredAgentTitle) { - this.scheduleStructuredJsonTurn(turn, structuredAgentTitle); } else if (shouldEmitPlanApprovalPrompt(prompt)) { this.schedulePlanApprovalTurn(turn); } else if (questionPrompt) { diff --git a/packages/server/src/server/session.test.ts b/packages/server/src/server/session.test.ts index 9e44adeff..baa4065fd 100644 --- a/packages/server/src/server/session.test.ts +++ b/packages/server/src/server/session.test.ts @@ -120,10 +120,6 @@ const agentResponseMocks = vi.hoisted(() => ({ generateStructuredAgentResponseWithFallback: vi.fn(), })); -const agentMetadataMocks = vi.hoisted(() => ({ - scheduleAgentMetadataGeneration: vi.fn(), -})); - const spawnMocks = vi.hoisted(() => ({ execCommand: vi.fn(), spawnWorkspaceScript: vi.fn(), @@ -194,14 +190,6 @@ vi.mock("./agent/agent-response-loop.js", async (importOriginal) => { }; }); -vi.mock("./agent/agent-metadata-generator.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - scheduleAgentMetadataGeneration: agentMetadataMocks.scheduleAgentMetadataGeneration, - }; -}); - vi.mock("./worktree-bootstrap.js", async (importOriginal) => { const actual = await importOriginal(); return { diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index 651c57efe..086c26fc5 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -3225,7 +3225,7 @@ export class Session { let createdAgentId: string | null = null; try { const trimmedPrompt = initialPrompt?.trim(); - const { explicitTitle, provisionalTitle } = resolveCreateAgentTitles({ + const { provisionalTitle } = resolveCreateAgentTitles({ configTitle: config.title, initialPrompt: trimmedPrompt, }); @@ -3259,9 +3259,7 @@ export class Session { logger: this.sessionLogger, paseoHome: this.paseoHome, worktreesRoot: this.worktreesRoot, - workspaceGitService: this.workspaceGitService, providerSnapshotManager: this.providerSnapshotManager, - daemonConfig: this.readStructuredGenerationDaemonConfig(), }, { kind: "session", @@ -3277,7 +3275,6 @@ export class Session { labels, env, provisionalTitle, - explicitTitle, firstAgentContext, buildSessionConfig: (sessionConfig, gitOptions, legacyWorktreeName, ctx) => this.buildAgentSessionConfig(sessionConfig, gitOptions, legacyWorktreeName, ctx), @@ -3455,10 +3452,6 @@ export class Session { workspaceId: workspace.workspaceId, agentManager: this.agentManager, agentStorage: this.agentStorage, - workspaceGitService: this.workspaceGitService, - providerSnapshotManager: this.providerSnapshotManager, - daemonConfig: this.readStructuredGenerationDaemonConfig(), - paseoHome: this.paseoHome, logger: this.sessionLogger, }); await this.registerWorkspaceForImportedAgent(workspace); diff --git a/packages/server/src/server/worktree-branch-name-generator.test.ts b/packages/server/src/server/worktree-branch-name-generator.test.ts index 10ccb729b..cd75753ec 100644 --- a/packages/server/src/server/worktree-branch-name-generator.test.ts +++ b/packages/server/src/server/worktree-branch-name-generator.test.ts @@ -193,7 +193,7 @@ describe("generateBranchNameFromFirstAgentContext", () => { ["paseo.json valid but missing metadataGeneration", {}], [ "metadataGeneration exists but missing branchName", - { metadataGeneration: { agentTitle: { instructions: "Use mb/." } } }, + { metadataGeneration: { commitMessage: { instructions: "Use Conventional Commits." } } }, ], ["branchName exists but instructions is undefined", { metadataGeneration: { branchName: {} } }], [ diff --git a/packages/server/src/utils/build-metadata-prompt.ts b/packages/server/src/utils/build-metadata-prompt.ts index a728c705d..b20795895 100644 --- a/packages/server/src/utils/build-metadata-prompt.ts +++ b/packages/server/src/utils/build-metadata-prompt.ts @@ -2,7 +2,7 @@ import { readPaseoConfigJson } from "./paseo-config-file.js"; import { PaseoConfigSchema } from "@getpaseo/protocol/paseo-config-schema"; import { wrapWithUserInstructions } from "./wrap-user-instructions.js"; -export type MetadataConfigKey = "agentTitle" | "branchName" | "commitMessage" | "pullRequest"; +export type MetadataConfigKey = "branchName" | "commitMessage" | "pullRequest"; export interface RepoRootResolver { resolveRepoRoot: (cwd: string) => Promise; diff --git a/public-docs/metadata-generation.md b/public-docs/metadata-generation.md index 48b9c9d6b..2e9d8d905 100644 --- a/public-docs/metadata-generation.md +++ b/public-docs/metadata-generation.md @@ -1,6 +1,6 @@ --- title: Metadata generation -description: How Paseo uses providers to generate agent titles, branch names, commit messages, and pull request text, and how to configure them. +description: How Paseo uses providers to generate branch names, commit messages, and pull request text, and how to configure them. nav: Metadata generation order: 42 category: Configuration @@ -10,9 +10,8 @@ category: Configuration Paseo asks a language model to write short pieces of text for you so you don't have to. This is separate from the agent you're talking to: it's a small, one-shot call made in the background. -Paseo generates four kinds of metadata: +Paseo generates three kinds of metadata: -- **Agent titles** — a short title for a new agent, derived from your first prompt. Only generated when you didn't type one yourself. - **Worktree branch names** — a slug for the branch a new worktree agent runs on. - **Commit messages** — a concise message for the changes you're committing. - **Pull request title and body** — drafted from the diff when you open a PR. @@ -67,7 +66,6 @@ You can steer the wording of each kind of metadata per repository with a `paseo. ```json { "metadataGeneration": { - "agentTitle": { "instructions": "Prefix titles with the area of the codebase." }, "branchName": { "instructions": "Use the format /-." }, "commitMessage": { "instructions": "Follow Conventional Commits." }, "pullRequest": { "instructions": "Include a Testing section in the body." }