fix(server): emit valid MCP list_agents payloads

Explicit `undefined` keys on optional snapshot fields were converted
to `null` by `ensureValidJson`, failing `AgentSnapshotPayloadSchema`
validation on the wire. Stop writing those keys so they remain absent,
which is what `.optional()` expects.

Also adds a test that parses `list_agents` output through the schema
to catch this class of bug.
This commit is contained in:
Mohamed Boudra
2026-04-16 18:29:34 +07:00
parent 7442323ca2
commit a2b743a6d3
3 changed files with 58 additions and 5 deletions

View File

@@ -94,7 +94,7 @@ export function toAgentPayload(
model: agent.config.model ?? null,
thinkingOptionId,
effectiveThinkingOptionId,
runtimeInfo,
...(runtimeInfo ? { runtimeInfo } : {}),
createdAt: agent.createdAt.toISOString(),
updatedAt: agent.updatedAt.toISOString(),
lastUserMessageAt: agent.lastUserMessageAt ? agent.lastUserMessageAt.toISOString() : null,
@@ -192,8 +192,6 @@ export function buildStoredAgentPayload(
availableModes: [],
pendingPermissions: [],
persistence: toAgentPersistenceHandle(logger, providerRegistry, record.persistence),
lastUsage: undefined,
lastError: undefined,
title: record.title ?? record.config?.title ?? null,
requiresAttention: record.requiresAttention ?? false,
attentionReason: record.attentionReason ?? null,

View File

@@ -2,12 +2,14 @@ import { describe, expect, it, vi } from "vitest";
import { mkdtemp, mkdir, rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { z } from "zod";
import { createTestLogger } from "../../test-utils/test-logger.js";
import { createAgentMcpServer } from "./mcp-server.js";
import type { AgentManager, ManagedAgent } from "./agent-manager.js";
import type { AgentStorage, StoredAgentRecord } from "./agent-storage.js";
import type { ProviderDefinition } from "./provider-registry.js";
import { AgentSnapshotPayloadSchema } from "../../shared/messages.js";
type TestDeps = {
agentManager: AgentManager;
@@ -702,6 +704,59 @@ describe("agent snapshot MCP serialization", () => {
]);
});
it("emits list_agents payloads that satisfy the declared output schema", async () => {
const { agentManager, agentStorage, spies } = createTestDeps();
const liveAgent = {
id: "live-agent",
provider: "claude",
cwd: "/tmp/live-project",
config: {},
runtimeInfo: undefined,
createdAt: new Date("2026-04-11T00:00:00.000Z"),
updatedAt: new Date("2026-04-11T00:00:00.000Z"),
lastUserMessageAt: null,
lifecycle: "idle",
capabilities: {
supportsStreaming: false,
supportsSessionPersistence: false,
supportsDynamicModes: false,
supportsMcpServers: true,
supportsReasoningStream: false,
supportsToolInvocations: true,
},
currentModeId: null,
availableModes: [],
features: [],
pendingPermissions: new Map(),
persistence: null,
labels: {},
attention: { requiresAttention: false },
} as unknown as ManagedAgent;
spies.agentManager.listAgents.mockReturnValue([liveAgent]);
spies.agentStorage.list.mockResolvedValue([
createStoredRecord({ id: "stored-non-archived", archivedAt: null }),
createStoredRecord({ id: "stored-archived", archivedAt: "2026-04-12T00:00:00.000Z" }),
]);
const server = await createAgentMcpServer({
agentManager,
agentStorage,
logger,
providerRegistry: {
claude: createProviderDefinition({}),
} as any,
});
const tool = (server as any)._registeredTools["list_agents"];
const response = await tool.callback({ includeArchived: true });
const parsed = z.array(AgentSnapshotPayloadSchema).safeParse(response.structuredContent.agents);
if (!parsed.success) {
throw new Error(
`list_agents response failed AgentSnapshotPayloadSchema: ${JSON.stringify(parsed.error.issues, null, 2)}`,
);
}
});
it("loads archived agents before reading get_agent_activity", async () => {
const { agentManager, agentStorage, spies } = createTestDeps();
const record = createStoredRecord({ id: "archived-activity-agent" });

View File

@@ -167,7 +167,7 @@ export function toAgentPersistenceHandle(
return {
provider,
sessionId: handle.sessionId,
nativeHandle: handle.nativeHandle,
metadata: handle.metadata,
...(handle.nativeHandle !== undefined ? { nativeHandle: handle.nativeHandle } : {}),
...(handle.metadata !== undefined ? { metadata: handle.metadata } : {}),
} satisfies AgentPersistenceHandle;
}