diff --git a/packages/server/src/server/agent/agent-manager.test.ts b/packages/server/src/server/agent/agent-manager.test.ts index d7302ef77..277fffe69 100644 --- a/packages/server/src/server/agent/agent-manager.test.ts +++ b/packages/server/src/server/agent/agent-manager.test.ts @@ -9,6 +9,7 @@ import { AgentManager } from "./agent-manager.js"; import { AgentStorage } from "./agent-storage.js"; import type { AgentClient, + AgentPersistenceHandle, AgentRunResult, AgentSession, AgentSessionConfig, @@ -149,6 +150,99 @@ describe("AgentManager", () => { ).rejects.toThrow("Working directory does not exist"); }); + test("resumeAgent keeps metadata config and applies systemPrompt/mcpServers overrides", async () => { + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-resume-")); + const storagePath = join(workdir, "agents"); + const storage = new AgentStorage(storagePath, logger); + + class ResumeCaptureClient implements AgentClient { + readonly provider = "codex" as const; + readonly capabilities = TEST_CAPABILITIES; + lastResumeOverrides: Partial | undefined; + + async isAvailable(): Promise { + return true; + } + + async createSession(config: AgentSessionConfig): Promise { + return new TestAgentSession(config); + } + + async resumeSession( + handle: AgentPersistenceHandle, + overrides?: Partial + ): Promise { + this.lastResumeOverrides = overrides; + const metadata = (handle.metadata ?? {}) as Partial; + const merged: AgentSessionConfig = { + ...metadata, + ...overrides, + provider: "codex", + cwd: overrides?.cwd ?? metadata.cwd ?? process.cwd(), + }; + return new TestAgentSession(merged); + } + } + + const client = new ResumeCaptureClient(); + const manager = new AgentManager({ + clients: { + codex: client, + }, + registry: storage, + logger, + idFactory: () => "00000000-0000-4000-8000-000000000106", + }); + + const handle: AgentPersistenceHandle = { + provider: "codex", + sessionId: "resume-session-1", + metadata: { + provider: "codex", + cwd: workdir, + systemPrompt: "old prompt", + mcpServers: { + legacy: { + type: "stdio", + command: "legacy-bridge", + args: ["/tmp/legacy.sock"], + }, + }, + }, + }; + + const resumed = await manager.resumeAgent(handle, { + cwd: workdir, + systemPrompt: "new prompt", + mcpServers: { + paseo: { + type: "stdio", + command: "node", + args: ["/tmp/mcp-bridge.mjs", "--socket", "/tmp/paseo.sock"], + }, + }, + }); + + expect(resumed.config.systemPrompt).toBe("new prompt"); + expect(resumed.config.mcpServers).toEqual({ + paseo: { + type: "stdio", + command: "node", + args: ["/tmp/mcp-bridge.mjs", "--socket", "/tmp/paseo.sock"], + }, + }); + expect(client.lastResumeOverrides).toMatchObject({ + systemPrompt: "new prompt", + mcpServers: { + paseo: { + type: "stdio", + command: "node", + args: ["/tmp/mcp-bridge.mjs", "--socket", "/tmp/paseo.sock"], + }, + }, + }); + }); + test("createAgent fails when generated agent ID is not a UUID", async () => { const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-")); const storagePath = join(workdir, "agents"); diff --git a/packages/server/src/server/agent/agent-projections.ts b/packages/server/src/server/agent/agent-projections.ts index e12f5c2c0..542bbf766 100644 --- a/packages/server/src/server/agent/agent-projections.ts +++ b/packages/server/src/server/agent/agent-projections.ts @@ -129,6 +129,9 @@ function buildSerializableConfig( if (extra !== undefined) { serializable.extra = extra; } + if (config.systemPrompt) { + serializable.systemPrompt = config.systemPrompt; + } if (config.mcpServers) { serializable.mcpServers = config.mcpServers; } diff --git a/packages/server/src/server/agent/agent-storage.test.ts b/packages/server/src/server/agent/agent-storage.test.ts index 4b0004d37..c3868167c 100644 --- a/packages/server/src/server/agent/agent-storage.test.ts +++ b/packages/server/src/server/agent/agent-storage.test.ts @@ -39,6 +39,7 @@ function createManagedAgent( modeId: configOverrides.modeId ?? "plan", model: configOverrides.model ?? "gpt-5.1", extra: configOverrides.extra ?? { claude: { maxThinkingTokens: 1024 } }, + systemPrompt: configOverrides.systemPrompt, mcpServers: configOverrides.mcpServers, }; const session = @@ -119,6 +120,7 @@ describe("AgentStorage", () => { config: { modeId: "coding", model: "gpt-5.1", + systemPrompt: "Be terse and explicit.", extra: { claude: { maxThinkingTokens: 1024 } }, mcpServers: { paseo: { @@ -137,6 +139,7 @@ describe("AgentStorage", () => { expect(record.provider).toBe("claude"); expect(record.config?.modeId).toBe("coding"); expect(record.config?.model).toBe("gpt-5.1"); + expect(record.config?.systemPrompt).toBe("Be terse and explicit."); expect(record.config?.mcpServers).toEqual({ paseo: { type: "stdio", diff --git a/packages/server/src/server/agent/agent-storage.ts b/packages/server/src/server/agent/agent-storage.ts index 0af305865..94f9e1462 100644 --- a/packages/server/src/server/agent/agent-storage.ts +++ b/packages/server/src/server/agent/agent-storage.ts @@ -15,6 +15,7 @@ const SERIALIZABLE_CONFIG_SCHEMA = z model: z.string().nullable().optional(), thinkingOptionId: z.string().nullable().optional(), extra: z.record(z.any()).nullable().optional(), + systemPrompt: z.string().nullable().optional(), mcpServers: z.record(z.any()).nullable().optional(), }) .nullable() @@ -62,7 +63,7 @@ const STORED_AGENT_SCHEMA = z.object({ export type SerializableAgentConfig = Pick< AgentSessionConfig, - "modeId" | "model" | "thinkingOptionId" | "extra" | "mcpServers" + "modeId" | "model" | "thinkingOptionId" | "extra" | "systemPrompt" | "mcpServers" >; export type StoredAgentRecord = z.infer; diff --git a/packages/server/src/server/agent/providers/claude-agent.ts b/packages/server/src/server/agent/providers/claude-agent.ts index c259712da..7e8608f2e 100644 --- a/packages/server/src/server/agent/providers/claude-agent.ts +++ b/packages/server/src/server/agent/providers/claude-agent.ts @@ -270,6 +270,9 @@ function coerceSessionMetadata(metadata: AgentMetadata | undefined): Partial { }); expect(applySnapshot).toHaveBeenCalledTimes(1); }); + + test("buildConfigOverrides carries systemPrompt and mcpServers", () => { + const record = createRecord({ + title: "Voice agent", + config: { + modeId: "default", + model: "gpt-5.1-codex-mini", + thinkingOptionId: "minimal", + systemPrompt: "Use speak first.", + mcpServers: { + paseo: { + type: "stdio", + command: "node", + args: ["/tmp/bridge.mjs", "--socket", "/tmp/agent.sock"], + }, + }, + }, + }); + + expect(buildConfigOverrides(record)).toMatchObject({ + cwd: "/tmp/project", + modeId: "plan", + model: "gpt-5.1-codex-mini", + thinkingOptionId: "minimal", + title: "Voice agent", + systemPrompt: "Use speak first.", + mcpServers: { + paseo: { + type: "stdio", + command: "node", + args: ["/tmp/bridge.mjs", "--socket", "/tmp/agent.sock"], + }, + }, + }); + }); + + test("buildSessionConfig includes persisted systemPrompt and mcpServers", () => { + const record = createRecord({ + provider: "codex", + config: { + modeId: "default", + model: "gpt-5.1-codex-mini", + systemPrompt: "Confirm and speak first.", + mcpServers: { + paseo: { + type: "stdio", + command: "node", + args: ["/tmp/bridge.mjs", "--socket", "/tmp/agent.sock"], + }, + }, + }, + }); + + expect(buildSessionConfig(record)).toMatchObject({ + provider: "codex", + cwd: "/tmp/project", + modeId: "plan", + model: "gpt-5.1-codex-mini", + systemPrompt: "Confirm and speak first.", + mcpServers: { + paseo: { + type: "stdio", + command: "node", + args: ["/tmp/bridge.mjs", "--socket", "/tmp/agent.sock"], + }, + }, + }); + }); }); diff --git a/packages/server/src/server/persistence-hooks.ts b/packages/server/src/server/persistence-hooks.ts index dc5a3b2ae..7f56e6841 100644 --- a/packages/server/src/server/persistence-hooks.ts +++ b/packages/server/src/server/persistence-hooks.ts @@ -56,6 +56,7 @@ export function buildConfigOverrides( thinkingOptionId: record.config?.thinkingOptionId ?? undefined, title: record.title ?? undefined, extra: record.config?.extra ?? undefined, + systemPrompt: record.config?.systemPrompt ?? undefined, mcpServers: record.config?.mcpServers ?? undefined, }; } @@ -75,6 +76,7 @@ export function buildSessionConfig( thinkingOptionId: overrides.thinkingOptionId, title: overrides.title, extra: overrides.extra, + systemPrompt: overrides.systemPrompt, mcpServers: overrides.mcpServers, }; } diff --git a/packages/server/src/shared/messages.ts b/packages/server/src/shared/messages.ts index 9acfcb49b..15a7c8eb7 100644 --- a/packages/server/src/shared/messages.ts +++ b/packages/server/src/shared/messages.ts @@ -105,6 +105,7 @@ const AgentSessionConfigSchema = z.object({ }) .partial() .optional(), + systemPrompt: z.string().optional(), mcpServers: z.record(McpServerConfigSchema).optional(), });