Update files

This commit is contained in:
Mohamed Boudra
2026-02-07 13:07:49 +07:00
parent b15691f13f
commit cef7d8c134
10 changed files with 182 additions and 1 deletions

View File

@@ -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<AgentSessionConfig> | undefined;
async isAvailable(): Promise<boolean> {
return true;
}
async createSession(config: AgentSessionConfig): Promise<AgentSession> {
return new TestAgentSession(config);
}
async resumeSession(
handle: AgentPersistenceHandle,
overrides?: Partial<AgentSessionConfig>
): Promise<AgentSession> {
this.lastResumeOverrides = overrides;
const metadata = (handle.metadata ?? {}) as Partial<AgentSessionConfig>;
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");

View File

@@ -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;
}

View File

@@ -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",

View File

@@ -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<typeof STORED_AGENT_SCHEMA>;

View File

@@ -270,6 +270,9 @@ function coerceSessionMetadata(metadata: AgentMetadata | undefined): Partial<Age
result.extra = extra;
}
}
if (typeof metadata.systemPrompt === "string") {
result.systemPrompt = metadata.systemPrompt;
}
if (isMcpServersRecord(metadata.mcpServers)) {
result.mcpServers = metadata.mcpServers;
}

View File

@@ -1597,6 +1597,9 @@ class CodexAppServerAgentSession implements AgentSession {
modeId: this.currentMode,
model: this.config.model ?? null,
thinkingOptionId,
extra: this.config.extra,
systemPrompt: this.config.systemPrompt,
mcpServers: this.config.mcpServers,
},
};
}

View File

@@ -1961,6 +1961,7 @@ const AgentSessionConfigSchema = z
networkAccess: z.boolean().optional(),
webSearch: z.boolean().optional(),
extra: AgentSessionExtraSchema.optional(),
systemPrompt: z.string().optional(),
mcpServers: z.record(McpServerConfigSchema).optional(),
})
.passthrough();

View File

@@ -4,6 +4,8 @@ import type { ManagedAgent } from "./agent/agent-manager.js";
import type { StoredAgentRecord } from "./agent/agent-storage.js";
import {
attachAgentStoragePersistence,
buildConfigOverrides,
buildSessionConfig,
} from "./persistence-hooks.js";
import type {
AgentPermissionRequest,
@@ -140,4 +142,72 @@ describe("persistence hooks", () => {
});
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"],
},
},
});
});
});

View File

@@ -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,
};
}

View File

@@ -105,6 +105,7 @@ const AgentSessionConfigSchema = z.object({
})
.partial()
.optional(),
systemPrompt: z.string().optional(),
mcpServers: z.record(McpServerConfigSchema).optional(),
});