Name agents by their first prompt line instead of an LLM summary (#1563)

* Name agents by their first prompt line instead of an LLM summary

The workspace title already captures the task; the LLM-generated agent
title just restated the same prompt. Drop the generator and keep the
provisional title (first prompt line), so the agent label stays
deterministic and explicit titles still win.

* Remove the no-op agent-title setting and leftover generator plumbing

Deleting the agent-title generator left its whole surface dangling: a
configurable "Agent titles" project setting that no longer did anything,
dead create-path plumbing, generator-only import/MCP deps, and the mock
provider's agent-title parser. Remove all of it, plus a dead test mock
and timing sleeps that guarded the deleted background job. Replace them
with a no-mock lifecycle test asserting the title stays the first prompt
line across create and settle.

Old paseo.json files with metadataGeneration.agentTitle still parse via
schema passthrough — see COMPAT(projectMetadataAgentTitle).
This commit is contained in:
Mohamed Boudra
2026-06-17 01:12:23 +08:00
committed by GitHub
parent e202ca5036
commit fcb7f8e215
31 changed files with 141 additions and 1035 deletions
-2
View File
@@ -1846,8 +1846,6 @@ export const ar: TranslationResources = {
metadata: { metadata: {
title: "توليد البيانات الوصفية", title: "توليد البيانات الوصفية",
info: "تعليمات خاصة بالمشروع يتم إدخالها في الذكاء الاصطناعي الذي يستخدمه Paseo لإنشاء بيانات التعريف - استخدمها لفرض اصطلاحات فريقك مثل تسمية الفرع أو نمط الالتزام أو تنسيق PR", info: "تعليمات خاصة بالمشروع يتم إدخالها في الذكاء الاصطناعي الذي يستخدمه Paseo لإنشاء بيانات التعريف - استخدمها لفرض اصطلاحات فريقك مثل تسمية الفرع أو نمط الالتزام أو تنسيق PR",
agentTitle: "عناوين Agent",
agentTitlePlaceholder: "اجعل العناوين ضرورية وأقل من 40 حرفًا",
branchName: "اسماء الفروع", branchName: "اسماء الفروع",
branchNamePlaceholder: "بادئة الفروع بـ fet/ أو Fix/, mb/ للفروع الشخصية", branchNamePlaceholder: "بادئة الفروع بـ fet/ أو Fix/, mb/ للفروع الشخصية",
commitMessage: "ارتكاب الرسائل", commitMessage: "ارتكاب الرسائل",
-2
View File
@@ -1854,8 +1854,6 @@ export const en = {
metadata: { metadata: {
title: "Metadata generation", 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", 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", branchName: "Branch names",
branchNamePlaceholder: "Prefix branches with feat/ or fix/, mb/ for personal branches", branchNamePlaceholder: "Prefix branches with feat/ or fix/, mb/ for personal branches",
commitMessage: "Commit messages", commitMessage: "Commit messages",
-2
View File
@@ -1887,8 +1887,6 @@ export const es: TranslationResources = {
metadata: { metadata: {
title: "Generación de metadatos", 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.", 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", branchName: "Nombres de sucursales",
branchNamePlaceholder: "Prefijo ramas con feat/ o fix/, mb/ para ramas personales", branchNamePlaceholder: "Prefijo ramas con feat/ o fix/, mb/ para ramas personales",
commitMessage: "Confirmar mensajes", commitMessage: "Confirmar mensajes",
-2
View File
@@ -1893,8 +1893,6 @@ export const fr: TranslationResources = {
metadata: { metadata: {
title: "Génération de métadonnées", 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.", 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", branchName: "Noms des succursales",
branchNamePlaceholder: branchNamePlaceholder:
"Préfixez les branches avec feat/ ou fix/, mb/ pour les branches personnelles", "Préfixez les branches avec feat/ ou fix/, mb/ pour les branches personnelles",
-2
View File
@@ -1879,8 +1879,6 @@ export const ru: TranslationResources = {
metadata: { metadata: {
title: "Генерация метаданных", title: "Генерация метаданных",
info: "Инструкции для конкретного проекта, внедренные в подсказки ИИ, которые Paseo использует для генерации метаданных. Используйте их для обеспечения соблюдения соглашений вашей команды, таких как наименование ветвей, стиль фиксации или формат PR.", info: "Инструкции для конкретного проекта, внедренные в подсказки ИИ, которые Paseo использует для генерации метаданных. Используйте их для обеспечения соблюдения соглашений вашей команды, таких как наименование ветвей, стиль фиксации или формат PR.",
agentTitle: "Agent заголовки",
agentTitlePlaceholder: "Сохраняйте заголовки обязательными и длиной не более 40 символов.",
branchName: "Названия ветвей", branchName: "Названия ветвей",
branchNamePlaceholder: "Префиксные ветки с feat/ или fix/, mb/ для личных веток", branchNamePlaceholder: "Префиксные ветки с feat/ или fix/, mb/ для личных веток",
commitMessage: "Фиксировать сообщения", commitMessage: "Фиксировать сообщения",
-2
View File
@@ -1823,8 +1823,6 @@ export const zhCN: TranslationResources = {
metadata: { metadata: {
title: "元数据生成", title: "元数据生成",
info: "注入到 Paseo 用来生成元数据的 AI prompts 中的 Project 专属指令,可用于强制执行团队约定,例如分支命名、提交风格或 PR 格式", info: "注入到 Paseo 用来生成元数据的 AI prompts 中的 Project 专属指令,可用于强制执行团队约定,例如分支命名、提交风格或 PR 格式",
agentTitle: "Agent 标题",
agentTitlePlaceholder: "标题保持祈使句且不超过 40 个字符",
branchName: "分支名称", branchName: "分支名称",
branchNamePlaceholder: "分支以 feat/ 或 fix/ 开头,个人分支使用 mb/", branchNamePlaceholder: "分支以 feat/ 或 fix/ 开头,个人分支使用 mb/",
commitMessage: "提交消息", commitMessage: "提交消息",
@@ -58,12 +58,6 @@ interface MetadataPromptField {
} }
const METADATA_PROMPT_FIELDS: Record<MetadataPromptKey, MetadataPromptField> = { const METADATA_PROMPT_FIELDS: Record<MetadataPromptKey, MetadataPromptField> = {
agentTitle: {
titleKey: "settings.project.metadata.agentTitle",
placeholderKey: "settings.project.metadata.agentTitlePlaceholder",
sectionTestID: "metadata-prompt-agent-title-section",
inputTestID: "metadata-prompt-agent-title-input",
},
branchName: { branchName: {
titleKey: "settings.project.metadata.branchName", titleKey: "settings.project.metadata.branchName",
placeholderKey: "settings.project.metadata.branchNamePlaceholder", placeholderKey: "settings.project.metadata.branchNamePlaceholder",
@@ -11,7 +11,6 @@ function emptyDraft(): ProjectConfigDraft {
teardownOriginalKind: "missing", teardownOriginalKind: "missing",
scripts: [], scripts: [],
metadataPrompts: { metadataPrompts: {
agentTitle: "",
branchName: "", branchName: "",
commitMessage: "", commitMessage: "",
pullRequest: "", pullRequest: "",
@@ -217,17 +216,15 @@ describe("applyDraftToConfig", () => {
expect(tunnel.port).toBe("auto"); 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({ const draft = configToDraft({
metadataGeneration: { metadataGeneration: {
agentTitle: { instructions: "Use mb/." },
branchName: { instructions: "feat/<slug>" }, branchName: { instructions: "feat/<slug>" },
commitMessage: { instructions: "Conventional commits." }, commitMessage: { instructions: "Conventional commits." },
pullRequest: { instructions: "Include risk notes." }, pullRequest: { instructions: "Include risk notes." },
}, },
}); });
expect(draft.metadataPrompts).toEqual({ expect(draft.metadataPrompts).toEqual({
agentTitle: "Use mb/.",
branchName: "feat/<slug>", branchName: "feat/<slug>",
commitMessage: "Conventional commits.", commitMessage: "Conventional commits.",
pullRequest: "Include risk notes.", pullRequest: "Include risk notes.",
@@ -236,11 +233,27 @@ describe("applyDraftToConfig", () => {
it("defaults metadata prompts to empty strings when not present", () => { it("defaults metadata prompts to empty strings when not present", () => {
const draft = configToDraft({ const draft = configToDraft({
metadataGeneration: { agentTitle: { instructions: "Use mb/." } }, metadataGeneration: { branchName: { instructions: "feat/<slug>" } },
}); });
expect(draft.metadataPrompts).toEqual({ expect(draft.metadataPrompts).toEqual({
agentTitle: "Use mb/.", branchName: "feat/<slug>",
branchName: "", 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/<slug>" },
},
}),
);
expect(draft.metadataPrompts).toEqual({
branchName: "feat/<slug>",
commitMessage: "", commitMessage: "",
pullRequest: "", pullRequest: "",
}); });
@@ -249,11 +262,11 @@ describe("applyDraftToConfig", () => {
it("writes only metadata prompt entries with non-empty text", () => { it("writes only metadata prompt entries with non-empty text", () => {
const base: PaseoConfigRaw = {}; const base: PaseoConfigRaw = {};
const draft = configToDraft(base); const draft = configToDraft(base);
draft.metadataPrompts.agentTitle = "Use mb/."; draft.metadataPrompts.branchName = "Use mb/.";
draft.metadataPrompts.commitMessage = "Conventional commits."; draft.metadataPrompts.commitMessage = "Conventional commits.";
const next = applyDraftToConfig({ draft, base }); const next = applyDraftToConfig({ draft, base });
expect(next.metadataGeneration).toEqual({ expect(next.metadataGeneration).toEqual({
agentTitle: { instructions: "Use mb/." }, branchName: { instructions: "Use mb/." },
commitMessage: { instructions: "Conventional commits." }, commitMessage: { instructions: "Conventional commits." },
}); });
}); });
@@ -261,16 +274,16 @@ describe("applyDraftToConfig", () => {
it("drops the metadataGeneration field when all prompts are empty", () => { it("drops the metadataGeneration field when all prompts are empty", () => {
const base = PaseoConfigRawSchema.parse({ const base = PaseoConfigRawSchema.parse({
metadataGeneration: { metadataGeneration: {
agentTitle: { instructions: "Use mb/." }, branchName: { instructions: "Use mb/." },
}, },
}); });
const draft = configToDraft(base); const draft = configToDraft(base);
draft.metadataPrompts.agentTitle = ""; draft.metadataPrompts.branchName = "";
const next = applyDraftToConfig({ draft, base }); const next = applyDraftToConfig({ draft, base });
expect(next.metadataGeneration).toBeUndefined(); 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({ const base = PaseoConfigRawSchema.parse({
metadataGeneration: { metadataGeneration: {
agentTitle: { instructions: "Use mb/." }, agentTitle: { instructions: "Use mb/." },
@@ -278,37 +291,38 @@ describe("applyDraftToConfig", () => {
}, },
}); });
const draft = configToDraft(base); 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 next = applyDraftToConfig({ draft, base });
const metadata = next.metadataGeneration as Record<string, unknown>; const metadata = next.metadataGeneration as Record<string, unknown>;
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); expect(metadata.futureField).toBe(42);
}); });
it("preserves unknown fields inside a metadata prompt entry on round-trip", () => { it("preserves unknown fields inside a metadata prompt entry on round-trip", () => {
const base = PaseoConfigRawSchema.parse({ const base = PaseoConfigRawSchema.parse({
metadataGeneration: { metadataGeneration: {
agentTitle: { instructions: "Use mb/.", model: "haiku" }, branchName: { instructions: "Use mb/.", model: "haiku" },
}, },
}); });
const draft = configToDraft(base); const draft = configToDraft(base);
draft.metadataPrompts.agentTitle = "Updated."; draft.metadataPrompts.branchName = "Updated.";
const next = applyDraftToConfig({ draft, base }); const next = applyDraftToConfig({ draft, base });
const metadata = next.metadataGeneration as Record<string, unknown>; const metadata = next.metadataGeneration as Record<string, unknown>;
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", () => { it("clears instructions but preserves unknown sibling fields when text becomes empty", () => {
const base = PaseoConfigRawSchema.parse({ const base = PaseoConfigRawSchema.parse({
metadataGeneration: { metadataGeneration: {
agentTitle: { instructions: "Use mb/.", model: "haiku" }, branchName: { instructions: "Use mb/.", model: "haiku" },
}, },
}); });
const draft = configToDraft(base); const draft = configToDraft(base);
draft.metadataPrompts.agentTitle = ""; draft.metadataPrompts.branchName = "";
const next = applyDraftToConfig({ draft, base }); const next = applyDraftToConfig({ draft, base });
const metadata = next.metadataGeneration as Record<string, unknown>; const metadata = next.metadataGeneration as Record<string, unknown>;
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", () => { it("drops scripts with an empty name and removes scripts no longer present in the draft", () => {
@@ -7,12 +7,7 @@ import type {
export type LifecycleOriginalKind = "string" | "array" | "missing"; export type LifecycleOriginalKind = "string" | "array" | "missing";
export const METADATA_PROMPT_KEYS = [ export const METADATA_PROMPT_KEYS = ["branchName", "commitMessage", "pullRequest"] as const;
"agentTitle",
"branchName",
"commitMessage",
"pullRequest",
] as const;
export type MetadataPromptKey = (typeof METADATA_PROMPT_KEYS)[number]; export type MetadataPromptKey = (typeof METADATA_PROMPT_KEYS)[number];
export interface ProjectScriptDraft { export interface ProjectScriptDraft {
@@ -105,7 +100,6 @@ function nextScriptDraftId(): string {
function emptyMetadataPrompts(): Record<MetadataPromptKey, string> { function emptyMetadataPrompts(): Record<MetadataPromptKey, string> {
return { return {
agentTitle: "",
branchName: "", branchName: "",
commitMessage: "", commitMessage: "",
pullRequest: "", pullRequest: "",
@@ -1,2 +1 @@
export const MAX_EXPLICIT_AGENT_TITLE_CHARS = 200; export const MAX_EXPLICIT_AGENT_TITLE_CHARS = 200;
export const MAX_AUTO_AGENT_TITLE_CHARS = 40;
@@ -37,7 +37,6 @@ describe("paseo config schema", () => {
expect( expect(
PaseoConfigSchema.parse({ PaseoConfigSchema.parse({
metadataGeneration: { metadataGeneration: {
agentTitle: { instructions: "Use concise titles." },
branchName: { instructions: "Prefix branches with feat/." }, branchName: { instructions: "Prefix branches with feat/." },
commitMessage: { instructions: "Use imperative mood." }, commitMessage: { instructions: "Use imperative mood." },
pullRequest: { instructions: "Include risk notes." }, pullRequest: { instructions: "Include risk notes." },
@@ -45,7 +44,6 @@ describe("paseo config schema", () => {
}), }),
).toEqual({ ).toEqual({
metadataGeneration: { metadataGeneration: {
agentTitle: { instructions: "Use concise titles." },
branchName: { instructions: "Prefix branches with feat/." }, branchName: { instructions: "Prefix branches with feat/." },
commitMessage: { instructions: "Use imperative mood." }, commitMessage: { instructions: "Use imperative mood." },
pullRequest: { instructions: "Include risk notes." }, pullRequest: { instructions: "Include risk notes." },
@@ -56,29 +54,40 @@ describe("paseo config schema", () => {
it("parses partial metadata generation instructions with missing entries undefined", () => { it("parses partial metadata generation instructions with missing entries undefined", () => {
const parsed = PaseoConfigSchema.parse({ const parsed = PaseoConfigSchema.parse({
metadataGeneration: { metadataGeneration: {
agentTitle: { instructions: "Keep it short." }, branchName: { instructions: "Keep it short." },
}, },
}); });
expect(parsed.metadataGeneration).toEqual({ 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?.commitMessage).toBeUndefined();
expect(parsed.metadataGeneration?.pullRequest).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", () => { it("passes through unknown metadata generation fields", () => {
expect( expect(
PaseoConfigSchema.parse({ PaseoConfigSchema.parse({
metadataGeneration: { metadataGeneration: {
agentTitle: { instructions: "Use concise titles." },
futureField: 42, futureField: 42,
}, },
}), }),
).toEqual({ ).toEqual({
metadataGeneration: { metadataGeneration: {
agentTitle: { instructions: "Use concise titles." },
futureField: 42, futureField: 42,
}, },
}); });
@@ -88,7 +97,7 @@ describe("paseo config schema", () => {
expect( expect(
PaseoConfigSchema.parse({ PaseoConfigSchema.parse({
metadataGeneration: { metadataGeneration: {
agentTitle: { branchName: {
instructions: "Use concise titles.", instructions: "Use concise titles.",
model: "haiku", model: "haiku",
}, },
@@ -96,7 +105,7 @@ describe("paseo config schema", () => {
}), }),
).toEqual({ ).toEqual({
metadataGeneration: { metadataGeneration: {
agentTitle: { branchName: {
instructions: "Use concise titles.", instructions: "Use concise titles.",
model: "haiku", model: "haiku",
}, },
@@ -108,17 +117,17 @@ describe("paseo config schema", () => {
expect( expect(
PaseoConfigSchema.parse({ PaseoConfigSchema.parse({
metadataGeneration: { metadataGeneration: {
agentTitle: { instructions: 42 }, branchName: { instructions: 42 },
}, },
}), }),
).toEqual({ ).toEqual({
metadataGeneration: { 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 = { const config = {
worktree: { worktree: {
setup: "npm install", setup: "npm install",
@@ -132,6 +141,7 @@ describe("paseo config schema", () => {
}, },
metadataGeneration: { metadataGeneration: {
agentTitle: { instructions: "Use concise titles." }, agentTitle: { instructions: "Use concise titles." },
branchName: { instructions: "Use concise branches." },
}, },
}; };
@@ -142,12 +152,12 @@ describe("paseo config schema", () => {
expect( expect(
PaseoConfigRawSchema.parse({ PaseoConfigRawSchema.parse({
metadataGeneration: { metadataGeneration: {
agentTitle: { instructions: 42 }, branchName: { instructions: 42 },
}, },
}), }),
).toEqual({ ).toEqual({
metadataGeneration: { metadataGeneration: {
agentTitle: {}, branchName: {},
}, },
}); });
}); });
+2 -1
View File
@@ -39,11 +39,12 @@ export const PaseoMetadataGenerationEntrySchema = z
export const PaseoMetadataGenerationSchema = z export const PaseoMetadataGenerationSchema = z
.object({ .object({
agentTitle: PaseoMetadataGenerationEntrySchema.optional(),
branchName: PaseoMetadataGenerationEntrySchema.optional(), branchName: PaseoMetadataGenerationEntrySchema.optional(),
commitMessage: PaseoMetadataGenerationEntrySchema.optional(), commitMessage: PaseoMetadataGenerationEntrySchema.optional(),
pullRequest: 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() .passthrough()
.catch({}); .catch({});
@@ -1862,86 +1862,6 @@ test("updateAgentMetadata bumps updatedAt for stored agents", async () => {
expect(Date.parse(after!.updatedAt)).toBeGreaterThan(Date.parse(before!.updatedAt)); 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 () => { test("persists live mode, model, and thinking changes without an external snapshot subscriber", async () => {
const workdir = mkdtempSync(join(tmpdir(), "agent-manager-live-persist-")); const workdir = mkdtempSync(join(tmpdir(), "agent-manager-live-persist-"));
const storagePath = join(workdir, "agents"); const storagePath = join(workdir, "agents");
@@ -1331,20 +1331,6 @@ export class AgentManager {
this.emitState(agent, { persist: false }); this.emitState(agent, { persist: false });
} }
async setGeneratedTitle(agentId: string, title: string): Promise<void> {
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<string, string>): Promise<void> { async setLabels(agentId: string, labels: Record<string, string>): Promise<void> {
const agent = this.requireAgent(agentId); const agent = this.requireAgent(agentId);
agent.labels = { ...agent.labels, ...labels }; agent.labels = { ...agent.labels, ...labels };
@@ -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<string> {
const results = new Set<string>();
try {
statSync(root);
} catch {
return results;
}
const stack: string[] = [root];
while (stack.length > 0) {
const dir = stack.pop()!;
let entries: ReturnType<typeof readdirSync<{ withFileTypes: true }>>;
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<string> {
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<string> {
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);
});
@@ -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<WorkspaceGitService, "resolveRepoRoot">;
providerSnapshotManager?: Pick<ProviderSnapshotManager, "listProviders">;
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<AgentMetadataNeeds> {
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<Record<string, z.ZodTypeAny>> | null {
if (!needs.needsTitle) {
return null;
}
const shape: Record<string, z.ZodTypeAny> = {};
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<WorkspaceGitService, "resolveRepoRoot">;
},
): Promise<string> {
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<void> {
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",
);
});
});
}
@@ -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("<user-instructions>");
const userInstructionIndex = prompt.indexOf("Use the prefix mb/.");
const closeTagIndex = prompt.indexOf("</user-instructions>");
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),
};
}
@@ -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 () => { test("applySnapshot accepts explicit title overrides", async () => {
const agentId = "agent-override"; const agentId = "agent-override";
await storage.applySnapshot(createManagedAgent({ id: agentId }), { title: "Provided Title" }); await storage.applySnapshot(createManagedAgent({ id: agentId }), { title: "Provided Title" });
@@ -226,22 +226,6 @@ export class AgentStorage {
await this.upsert({ ...record, title }); await this.upsert({ ...record, title });
} }
async setGeneratedTitle(agentId: string, title: string): Promise<StoredAgentRecord> {
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<void> { async flush(): Promise<void> {
await this.load().catch(() => undefined); await this.load().catch(() => undefined);
const writes = Array.from(this.pendingWrites.values()); const writes = Array.from(this.pendingWrites.values());
@@ -70,7 +70,6 @@ test("session create forwards clientMessageId to the initial prompt run options"
clientMessageId: "msg-create-1", clientMessageId: "msg-create-1",
labels: {}, labels: {},
provisionalTitle: null, provisionalTitle: null,
explicitTitle: "Explicit title",
firstAgentContext: { attachments: [] }, firstAgentContext: { attachments: [] },
buildSessionConfig: async (config) => ({ sessionConfig: config }), buildSessionConfig: async (config) => ({ sessionConfig: config }),
}); });
@@ -99,7 +98,6 @@ test("session create stamps the requested workspaceId when no worktree setup run
workspaceId: "ws-source", workspaceId: "ws-source",
labels: {}, labels: {},
provisionalTitle: null, provisionalTitle: null,
explicitTitle: null,
firstAgentContext: { attachments: [] }, firstAgentContext: { attachments: [] },
buildSessionConfig: async (config) => ({ sessionConfig: config }), 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", workspaceId: "ws-source",
labels: {}, labels: {},
provisionalTitle: null, provisionalTitle: null,
explicitTitle: null,
firstAgentContext: { attachments: [] }, firstAgentContext: { attachments: [] },
buildSessionConfig: async (config) => ({ buildSessionConfig: async (config) => ({
sessionConfig: config, sessionConfig: config,
@@ -163,7 +160,6 @@ test("mcp create stamps the new worktree's workspaceId, not the parent's", async
workspaceId: "ws-parent", workspaceId: "ws-parent",
labels: {}, labels: {},
provisionalTitle: null, provisionalTitle: null,
explicitTitle: null,
firstAgentContext: { attachments: [] }, firstAgentContext: { attachments: [] },
buildSessionConfig: async (config) => ({ sessionConfig: config }), 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 }); 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 });
}
});
@@ -5,7 +5,6 @@ import type { TerminalManager } from "../../../terminal/terminal-manager.js";
import type { CreatePaseoWorktreeInput } from "../../paseo-worktree-service.js"; import type { CreatePaseoWorktreeInput } from "../../paseo-worktree-service.js";
import { expandUserPath, resolvePathFromBase } from "../../path-utils.js"; import { expandUserPath, resolvePathFromBase } from "../../path-utils.js";
import { toWorktreeRequestError } from "../../worktree-errors.js"; import { toWorktreeRequestError } from "../../worktree-errors.js";
import type { WorkspaceGitService } from "../../workspace-git-service.js";
import type { import type {
AgentWorktreeSetupContinuation, AgentWorktreeSetupContinuation,
CreatePaseoWorktreeSetupContinuationInput, CreatePaseoWorktreeSetupContinuationInput,
@@ -14,8 +13,6 @@ import type {
} from "../../worktree-session.js"; } from "../../worktree-session.js";
import type { AgentAttachment, FirstAgentContext, GitSetupOptions } from "../../messages.js"; import type { AgentAttachment, FirstAgentContext, GitSetupOptions } from "../../messages.js";
import type { AgentManager, ManagedAgent } from "../agent-manager.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 { import type {
AgentPromptContentBlock, AgentPromptContentBlock,
AgentPromptInput, AgentPromptInput,
@@ -46,13 +43,8 @@ interface CreateAgentCommandDependencies {
logger: Logger; logger: Logger;
paseoHome?: string; paseoHome?: string;
worktreesRoot?: string; worktreesRoot?: string;
workspaceGitService?: Pick<
WorkspaceGitService,
"getSnapshot" | "listWorktrees" | "resolveRepoRoot"
>;
terminalManager?: TerminalManager | null; terminalManager?: TerminalManager | null;
providerSnapshotManager: ProviderSnapshotManager; providerSnapshotManager: ProviderSnapshotManager;
daemonConfig?: StructuredGenerationDaemonConfig | null;
createPaseoWorktree?: CreatePaseoWorktreeWorkflowFn; createPaseoWorktree?: CreatePaseoWorktreeWorkflowFn;
// Mints a fresh workspace for a cwd and returns its id. Used when an agent is // 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 // created with no parent and no worktree: it owns a brand-new workspace rather
@@ -74,7 +66,6 @@ export interface CreateAgentFromSessionInput {
labels: Record<string, string>; labels: Record<string, string>;
env?: Record<string, string>; env?: Record<string, string>;
provisionalTitle: string | null; provisionalTitle: string | null;
explicitTitle: string | null;
firstAgentContext: FirstAgentContext; firstAgentContext: FirstAgentContext;
buildSessionConfig: ( buildSessionConfig: (
config: AgentSessionConfig, config: AgentSessionConfig,
@@ -124,10 +115,8 @@ export interface CreateAgentCommandResult {
interface ResolvedCreateAgent { interface ResolvedCreateAgent {
config: AgentSessionConfig; config: AgentSessionConfig;
createOptions?: AgentCreateOptions; createOptions?: AgentCreateOptions;
metadataInitialPrompt?: string;
prompt?: AgentPromptInput; prompt?: AgentPromptInput;
runOptions?: AgentRunOptions; runOptions?: AgentRunOptions;
explicitTitle: string | null;
setupContinuation?: AgentWorktreeSetupContinuation; setupContinuation?: AgentWorktreeSetupContinuation;
background: boolean; background: boolean;
promptFailure: "throw" | "log"; promptFailure: "throw" | "log";
@@ -221,10 +210,8 @@ async function resolveSessionCreateAgent(
// path). createdWorkspaceId is the freshly created worktree's workspace. // path). createdWorkspaceId is the freshly created worktree's workspace.
workspaceId: setupContinuation ? createdWorkspaceId : input.workspaceId, workspaceId: setupContinuation ? createdWorkspaceId : input.workspaceId,
}, },
metadataInitialPrompt: trimmedPrompt,
prompt: hasPromptContent ? prompt : undefined, prompt: hasPromptContent ? prompt : undefined,
runOptions, runOptions,
explicitTitle: input.explicitTitle,
setupContinuation, setupContinuation,
background: true, background: true,
promptFailure: "throw", promptFailure: "throw",
@@ -303,9 +290,7 @@ async function resolveMcpCreateAgent(
...(workspaceId ? { workspaceId } : {}), ...(workspaceId ? { workspaceId } : {}),
} }
: undefined, : undefined,
metadataInitialPrompt: trimmedPrompt,
prompt: trimmedPrompt, prompt: trimmedPrompt,
explicitTitle: input.title.trim(),
setupContinuation, setupContinuation,
background: input.background, background: input.background,
promptFailure: "log", promptFailure: "log",
@@ -327,25 +312,6 @@ async function sendInitialPrompt(
resolved: ResolvedCreateAgent, resolved: ResolvedCreateAgent,
snapshot: ManagedAgent, snapshot: ManagedAgent,
): Promise<{ started: boolean; liveSnapshot: 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 { try {
const prompt = resolved.prompt; const prompt = resolved.prompt;
if (prompt === undefined) { if (prompt === undefined) {
@@ -379,7 +379,6 @@ test("importProviderSession imports a selected provider session without listing"
list: vi.fn().mockResolvedValue([]), list: vi.fn().mockResolvedValue([]),
get: vi.fn().mockResolvedValue(null), get: vi.fn().mockResolvedValue(null),
} as unknown as AgentStorage; } as unknown as AgentStorage;
const scheduleAgentMetadataGeneration = vi.fn();
const result = await importProviderSession({ const result = await importProviderSession({
request: { request: {
@@ -392,7 +391,6 @@ test("importProviderSession imports a selected provider session without listing"
agentManager, agentManager,
agentStorage, agentStorage,
logger: { warn: vi.fn(), error: vi.fn() } as never, logger: { warn: vi.fn(), error: vi.fn() } as never,
deps: { scheduleAgentMetadataGeneration },
}); });
expect(agentManager.importProviderSession).toHaveBeenCalledWith({ expect(agentManager.importProviderSession).toHaveBeenCalledWith({
@@ -402,15 +400,6 @@ test("importProviderSession imports a selected provider session without listing"
workspaceId: "ws-imported", workspaceId: "ws-imported",
labels: undefined, 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 }); expect(result).toEqual({ snapshot, timelineSize: 2 });
}); });
@@ -7,14 +7,7 @@ import type {
ManagedImportableProviderSession, ManagedImportableProviderSession,
} from "./agent-manager.js"; } from "./agent-manager.js";
import type { AgentStorage, StoredAgentRecord } from "./agent-storage.js"; import type { AgentStorage, StoredAgentRecord } from "./agent-storage.js";
import type { import type { AgentPersistenceHandle, AgentProvider } from "./agent-sdk-types.js";
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 { unarchiveAgentState } from "./agent-prompt.js"; import { unarchiveAgentState } from "./agent-prompt.js";
import { toRecentProviderSessionDescriptorPayload } from "./agent-projections.js"; import { toRecentProviderSessionDescriptorPayload } from "./agent-projections.js";
import type { import type {
@@ -22,7 +15,6 @@ import type {
ImportAgentRequestMessageSchema, ImportAgentRequestMessageSchema,
RecentProviderSessionDescriptorPayload, RecentProviderSessionDescriptorPayload,
} from "@getpaseo/protocol/messages"; } from "@getpaseo/protocol/messages";
import type { WorkspaceGitService } from "../workspace-git-service.js";
import { createRealpathAwarePathMatcher } from "../../utils/path.js"; import { createRealpathAwarePathMatcher } from "../../utils/path.js";
type ImportAgentRequestMessage = z.infer<typeof ImportAgentRequestMessageSchema>; type ImportAgentRequestMessage = z.infer<typeof ImportAgentRequestMessageSchema>;
@@ -65,14 +57,7 @@ export interface ImportProviderSessionInput {
workspaceId: string; workspaceId: string;
agentManager: AgentManager; agentManager: AgentManager;
agentStorage: AgentStorage; agentStorage: AgentStorage;
workspaceGitService?: Pick<WorkspaceGitService, "resolveRepoRoot">;
providerSnapshotManager?: Pick<ProviderSnapshotManager, "listProviders">;
daemonConfig?: StructuredGenerationDaemonConfig | null;
paseoHome?: string;
logger: Logger; logger: Logger;
deps?: {
scheduleAgentMetadataGeneration?: typeof scheduleAgentMetadataGeneration;
};
} }
export interface ImportProviderSessionResult { export interface ImportProviderSessionResult {
@@ -168,17 +153,6 @@ export async function importProviderSession(
labels, labels,
}); });
await unarchiveAgentState(input.agentStorage, input.agentManager, snapshot.id); 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 { return {
snapshot, snapshot,
@@ -204,48 +178,6 @@ async function unarchiveAgentByHandle(
await unarchiveAgentState(agentStorage, agentManager, matched.id); await unarchiveAgentState(agentStorage, agentManager, matched.id);
} }
function scheduleImportedAgentMetadata(input: {
snapshot: ManagedAgent;
agentManager: AgentManager;
workspaceGitService?: Pick<WorkspaceGitService, "resolveRepoRoot">;
providerSnapshotManager?: Pick<ProviderSnapshotManager, "listProviders">;
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 { function parseRecentProviderSessionsSince(since: string | undefined): number | null {
if (!since) { if (!since) {
return null; 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( async function collectImportedProviderSessionHandles(
agentManager: Pick<AgentManager, "listAgents">, agentManager: Pick<AgentManager, "listAgents">,
agentStorage: Pick<AgentStorage, "list">, agentStorage: Pick<AgentStorage, "list">,
@@ -944,7 +944,6 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
logger: childLogger, logger: childLogger,
paseoHome: options.paseoHome, paseoHome: options.paseoHome,
worktreesRoot: options.worktreesRoot, worktreesRoot: options.worktreesRoot,
workspaceGitService: options.workspaceGitService,
terminalManager, terminalManager,
providerSnapshotManager, providerSnapshotManager,
createPaseoWorktree: options.createPaseoWorktree, createPaseoWorktree: options.createPaseoWorktree,
@@ -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 () => { test("emits sub-word tokens, reasoning, and sequential tool calls during a foreground turn", async () => {
vi.useFakeTimers(); vi.useFakeTimers();
const client = new MockLoadTestAgentClient(); const client = new MockLoadTestAgentClient();
@@ -311,37 +311,6 @@ function parseStructuredBranchNamePrompt(
return { title: title || "Mock task", branch }; 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 { function buildRepeatedPayload(bytes: number, prefix: string): string {
const line = `${prefix} ${"x".repeat(96)}\n`; const line = `${prefix} ${"x".repeat(96)}\n`;
let output = ""; let output = "";
@@ -678,11 +647,8 @@ export class MockLoadTestAgentSession implements AgentSession {
const stress = parseAgentStreamStressPrompt(prompt); const stress = parseAgentStreamStressPrompt(prompt);
const questionPrompt = parseMockQuestionPrompt(prompt); const questionPrompt = parseMockQuestionPrompt(prompt);
const structuredBranchName = parseStructuredBranchNamePrompt(prompt); const structuredBranchName = parseStructuredBranchNamePrompt(prompt);
const structuredAgentTitle = parseStructuredAgentTitlePrompt(prompt);
if (structuredBranchName) { if (structuredBranchName) {
this.scheduleStructuredJsonTurn(turn, structuredBranchName); this.scheduleStructuredJsonTurn(turn, structuredBranchName);
} else if (structuredAgentTitle) {
this.scheduleStructuredJsonTurn(turn, structuredAgentTitle);
} else if (shouldEmitPlanApprovalPrompt(prompt)) { } else if (shouldEmitPlanApprovalPrompt(prompt)) {
this.schedulePlanApprovalTurn(turn); this.schedulePlanApprovalTurn(turn);
} else if (questionPrompt) { } else if (questionPrompt) {
@@ -120,10 +120,6 @@ const agentResponseMocks = vi.hoisted(() => ({
generateStructuredAgentResponseWithFallback: vi.fn(), generateStructuredAgentResponseWithFallback: vi.fn(),
})); }));
const agentMetadataMocks = vi.hoisted(() => ({
scheduleAgentMetadataGeneration: vi.fn(),
}));
const spawnMocks = vi.hoisted(() => ({ const spawnMocks = vi.hoisted(() => ({
execCommand: vi.fn(), execCommand: vi.fn(),
spawnWorkspaceScript: 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<typeof import("./agent/agent-metadata-generator.js")>();
return {
...actual,
scheduleAgentMetadataGeneration: agentMetadataMocks.scheduleAgentMetadataGeneration,
};
});
vi.mock("./worktree-bootstrap.js", async (importOriginal) => { vi.mock("./worktree-bootstrap.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./worktree-bootstrap.js")>(); const actual = await importOriginal<typeof import("./worktree-bootstrap.js")>();
return { return {
+1 -8
View File
@@ -3225,7 +3225,7 @@ export class Session {
let createdAgentId: string | null = null; let createdAgentId: string | null = null;
try { try {
const trimmedPrompt = initialPrompt?.trim(); const trimmedPrompt = initialPrompt?.trim();
const { explicitTitle, provisionalTitle } = resolveCreateAgentTitles({ const { provisionalTitle } = resolveCreateAgentTitles({
configTitle: config.title, configTitle: config.title,
initialPrompt: trimmedPrompt, initialPrompt: trimmedPrompt,
}); });
@@ -3259,9 +3259,7 @@ export class Session {
logger: this.sessionLogger, logger: this.sessionLogger,
paseoHome: this.paseoHome, paseoHome: this.paseoHome,
worktreesRoot: this.worktreesRoot, worktreesRoot: this.worktreesRoot,
workspaceGitService: this.workspaceGitService,
providerSnapshotManager: this.providerSnapshotManager, providerSnapshotManager: this.providerSnapshotManager,
daemonConfig: this.readStructuredGenerationDaemonConfig(),
}, },
{ {
kind: "session", kind: "session",
@@ -3277,7 +3275,6 @@ export class Session {
labels, labels,
env, env,
provisionalTitle, provisionalTitle,
explicitTitle,
firstAgentContext, firstAgentContext,
buildSessionConfig: (sessionConfig, gitOptions, legacyWorktreeName, ctx) => buildSessionConfig: (sessionConfig, gitOptions, legacyWorktreeName, ctx) =>
this.buildAgentSessionConfig(sessionConfig, gitOptions, legacyWorktreeName, ctx), this.buildAgentSessionConfig(sessionConfig, gitOptions, legacyWorktreeName, ctx),
@@ -3455,10 +3452,6 @@ export class Session {
workspaceId: workspace.workspaceId, workspaceId: workspace.workspaceId,
agentManager: this.agentManager, agentManager: this.agentManager,
agentStorage: this.agentStorage, agentStorage: this.agentStorage,
workspaceGitService: this.workspaceGitService,
providerSnapshotManager: this.providerSnapshotManager,
daemonConfig: this.readStructuredGenerationDaemonConfig(),
paseoHome: this.paseoHome,
logger: this.sessionLogger, logger: this.sessionLogger,
}); });
await this.registerWorkspaceForImportedAgent(workspace); await this.registerWorkspaceForImportedAgent(workspace);
@@ -193,7 +193,7 @@ describe("generateBranchNameFromFirstAgentContext", () => {
["paseo.json valid but missing metadataGeneration", {}], ["paseo.json valid but missing metadataGeneration", {}],
[ [
"metadataGeneration exists but missing branchName", "metadataGeneration exists but missing branchName",
{ metadataGeneration: { agentTitle: { instructions: "Use mb/." } } }, { metadataGeneration: { commitMessage: { instructions: "Use Conventional Commits." } } },
], ],
["branchName exists but instructions is undefined", { metadataGeneration: { branchName: {} } }], ["branchName exists but instructions is undefined", { metadataGeneration: { branchName: {} } }],
[ [
@@ -2,7 +2,7 @@ import { readPaseoConfigJson } from "./paseo-config-file.js";
import { PaseoConfigSchema } from "@getpaseo/protocol/paseo-config-schema"; import { PaseoConfigSchema } from "@getpaseo/protocol/paseo-config-schema";
import { wrapWithUserInstructions } from "./wrap-user-instructions.js"; import { wrapWithUserInstructions } from "./wrap-user-instructions.js";
export type MetadataConfigKey = "agentTitle" | "branchName" | "commitMessage" | "pullRequest"; export type MetadataConfigKey = "branchName" | "commitMessage" | "pullRequest";
export interface RepoRootResolver { export interface RepoRootResolver {
resolveRepoRoot: (cwd: string) => Promise<string>; resolveRepoRoot: (cwd: string) => Promise<string>;
+2 -4
View File
@@ -1,6 +1,6 @@
--- ---
title: Metadata generation 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 nav: Metadata generation
order: 42 order: 42
category: Configuration 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 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. - **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. - **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. - **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 ```json
{ {
"metadataGeneration": { "metadataGeneration": {
"agentTitle": { "instructions": "Prefix titles with the area of the codebase." },
"branchName": { "instructions": "Use the format <type>/<scope>-<short-desc>." }, "branchName": { "instructions": "Use the format <type>/<scope>-<short-desc>." },
"commitMessage": { "instructions": "Follow Conventional Commits." }, "commitMessage": { "instructions": "Follow Conventional Commits." },
"pullRequest": { "instructions": "Include a Testing section in the body." } "pullRequest": { "instructions": "Include a Testing section in the body." }