refactor: derive session payloads from ManagedAgent

This commit is contained in:
Mohamed Boudra
2025-11-30 00:14:00 +00:00
parent 7aec973323
commit e2924f7523
3 changed files with 30 additions and 116 deletions
+12 -10
View File
@@ -9,7 +9,7 @@ import type {
} from "./agent-sdk-types.js"; } from "./agent-sdk-types.js";
import type { import type {
AgentManager, AgentManager,
AgentSnapshot, ManagedAgent,
WaitForAgentResult, WaitForAgentResult,
} from "./agent-manager.js"; } from "./agent-manager.js";
import { import {
@@ -19,6 +19,7 @@ import {
AgentSnapshotPayloadSchema, AgentSnapshotPayloadSchema,
serializeAgentSnapshot, serializeAgentSnapshot,
} from "../messages.js"; } from "../messages.js";
import { toAgentPayload } from "./agent-projections.js";
import { curateAgentActivity } from "./activity-curator.js"; import { curateAgentActivity } from "./activity-curator.js";
import { AGENT_PROVIDER_DEFINITIONS } from "./provider-manifest.js"; import { AGENT_PROVIDER_DEFINITIONS } from "./provider-manifest.js";
import { AgentRegistry } from "./agent-registry.js"; import { AgentRegistry } from "./agent-registry.js";
@@ -90,7 +91,7 @@ async function resolveAgentTitle(
async function serializeSnapshotWithMetadata( async function serializeSnapshotWithMetadata(
agentRegistry: AgentRegistry, agentRegistry: AgentRegistry,
snapshot: AgentSnapshot snapshot: ManagedAgent
) { ) {
const title = await resolveAgentTitle(agentRegistry, snapshot.id); const title = await resolveAgentTitle(agentRegistry, snapshot.id);
return serializeAgentSnapshot(snapshot, { title }); return serializeAgentSnapshot(snapshot, { title });
@@ -232,7 +233,7 @@ export async function createAgentMcpServer(
structuredContent: ensureValidJson({ structuredContent: ensureValidJson({
agentId: snapshot.id, agentId: snapshot.id,
type: provider, type: provider,
status: snapshot.status, status: snapshot.lifecycle,
cwd: snapshot.cwd, cwd: snapshot.cwd,
currentModeId: snapshot.currentModeId, currentModeId: snapshot.currentModeId,
availableModes: snapshot.availableModes, availableModes: snapshot.availableModes,
@@ -394,7 +395,7 @@ export async function createAgentMcpServer(
const responseData = { const responseData = {
success: true, success: true,
status: snapshot?.status ?? "idle", status: snapshot?.lifecycle ?? "idle",
lastMessage: null, lastMessage: null,
permission: null, permission: null,
}; };
@@ -435,7 +436,7 @@ export async function createAgentMcpServer(
return { return {
content: [], content: [],
structuredContent: ensureValidJson({ structuredContent: ensureValidJson({
status: snapshot.status, status: snapshot.lifecycle,
snapshot: structuredSnapshot, snapshot: structuredSnapshot,
}), }),
}; };
@@ -616,13 +617,14 @@ export async function createAgentMcpServer(
}, },
}, },
async () => { async () => {
const permissions = agentManager.listAgents().flatMap((agent) => const permissions = agentManager.listAgents().flatMap((agent) => {
agent.pendingPermissions.map((request) => ({ const payload = toAgentPayload(agent);
return payload.pendingPermissions.map((request) => ({
agentId: agent.id, agentId: agent.id,
status: agent.status, status: payload.status,
request, request,
})) }));
); });
return { return {
content: [], content: [],
+5 -93
View File
@@ -1,8 +1,9 @@
import { z } from "zod"; import { z } from "zod";
import { import {
AGENT_LIFECYCLE_STATUSES, AGENT_LIFECYCLE_STATUSES,
type AgentSnapshot, type ManagedAgent,
} from "./agent/agent-manager.js"; } from "./agent/agent-manager.js";
import { toAgentPayload } from "./agent/agent-projections.js";
import type { import type {
AgentCapabilityFlags, AgentCapabilityFlags,
AgentModelDefinition, AgentModelDefinition,
@@ -16,16 +17,6 @@ import type {
AgentUsage, AgentUsage,
} from "./agent/agent-sdk-types.js"; } from "./agent/agent-sdk-types.js";
export type AgentSnapshotPayload = Omit<
AgentSnapshot,
"createdAt" | "updatedAt" | "lastUserMessageAt"
> & {
createdAt: string;
updatedAt: string;
lastUserMessageAt: string | null;
title: string | null;
};
const AGENT_PROVIDERS: [AgentProvider, AgentProvider] = ["claude", "codex"]; const AGENT_PROVIDERS: [AgentProvider, AgentProvider] = ["claude", "codex"];
const AgentProviderSchema = z.enum(AGENT_PROVIDERS); const AgentProviderSchema = z.enum(AGENT_PROVIDERS);
@@ -230,96 +221,17 @@ export const AgentSnapshotPayloadSchema = z.object({
title: z.string().nullable(), title: z.string().nullable(),
}); });
function sanitizeOptionalJson(value: unknown): unknown { export type AgentSnapshotPayload = z.infer<typeof AgentSnapshotPayloadSchema>;
if (value === undefined) {
return undefined;
}
if (value === null) {
return null;
}
if (Array.isArray(value)) {
return value
.map((item) => sanitizeOptionalJson(item))
.filter((item) => item !== undefined);
}
if (value instanceof Date) {
return value.toISOString();
}
if (typeof value === "object") {
const result: Record<string, unknown> = {};
for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
const sanitized = sanitizeOptionalJson(val);
if (sanitized !== undefined) {
result[key] = sanitized;
}
}
return Object.keys(result).length ? result : undefined;
}
return value;
}
function sanitizeOptionalJsonValue<T>(value: T | undefined): T | undefined {
const sanitized = sanitizeOptionalJson(value);
return sanitized === undefined ? undefined : (sanitized as T);
}
function sanitizePersistenceHandle(
handle: AgentPersistenceHandle | null
): AgentPersistenceHandle | null {
if (!handle) {
return null;
}
const sanitized: AgentPersistenceHandle = {
provider: handle.provider,
sessionId: handle.sessionId,
};
if (handle.nativeHandle !== undefined) {
sanitized.nativeHandle = handle.nativeHandle;
}
const metadata = sanitizeOptionalJsonValue(handle.metadata);
if (metadata !== undefined) {
sanitized.metadata = metadata;
}
return sanitized;
}
export type AgentStreamEventPayload = z.infer< export type AgentStreamEventPayload = z.infer<
typeof AgentStreamEventPayloadSchema typeof AgentStreamEventPayloadSchema
>; >;
export function serializeAgentSnapshot( export function serializeAgentSnapshot(
snapshot: AgentSnapshot, agent: ManagedAgent,
options?: { title?: string | null } options?: { title?: string | null }
): AgentSnapshotPayload { ): AgentSnapshotPayload {
const payload: AgentSnapshotPayload = { return toAgentPayload(agent, options);
id: snapshot.id,
provider: snapshot.provider,
cwd: snapshot.cwd,
model: snapshot.model,
createdAt: snapshot.createdAt.toISOString(),
updatedAt: snapshot.updatedAt.toISOString(),
lastUserMessageAt: snapshot.lastUserMessageAt
? snapshot.lastUserMessageAt.toISOString()
: null,
status: snapshot.status,
sessionId: snapshot.sessionId,
capabilities: snapshot.capabilities,
currentModeId: snapshot.currentModeId,
availableModes: snapshot.availableModes,
pendingPermissions: snapshot.pendingPermissions,
persistence: sanitizePersistenceHandle(snapshot.persistence),
title: options?.title ?? null,
};
const lastUsage = sanitizeOptionalJsonValue<AgentUsage>(snapshot.lastUsage);
if (lastUsage !== undefined) {
payload.lastUsage = lastUsage;
}
if (snapshot.lastError !== undefined) {
payload.lastError = snapshot.lastError;
}
return payload;
} }
export function serializeAgentStreamEvent( export function serializeAgentStreamEvent(
+13 -13
View File
@@ -12,7 +12,6 @@ import {
OpenRouterProviderOptions, OpenRouterProviderOptions,
} from "@openrouter/ai-sdk-provider"; } from "@openrouter/ai-sdk-provider";
import { import {
serializeAgentSnapshot,
serializeAgentStreamEvent, serializeAgentStreamEvent,
type AgentSnapshotPayload, type AgentSnapshotPayload,
type SessionInboundMessage, type SessionInboundMessage,
@@ -39,7 +38,8 @@ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { createTerminalMcpServer } from "./terminal-mcp/index.js"; import { createTerminalMcpServer } from "./terminal-mcp/index.js";
import { fetchProviderModelCatalog } from "./agent/model-catalog.js"; import { fetchProviderModelCatalog } from "./agent/model-catalog.js";
import { AgentManager } from "./agent/agent-manager.js"; import { AgentManager } from "./agent/agent-manager.js";
import type { AgentSnapshot } from "./agent/agent-manager.js"; import type { ManagedAgent } from "./agent/agent-manager.js";
import { toAgentPayload } from "./agent/agent-projections.js";
import type { import type {
AgentPermissionResponse, AgentPermissionResponse,
AgentPromptInput, AgentPromptInput,
@@ -234,7 +234,7 @@ export class Session {
private unsubscribeAgentEvents: (() => void) | null = null; private unsubscribeAgentEvents: (() => void) | null = null;
private pendingAgentInitializations: Map< private pendingAgentInitializations: Map<
string, string,
Promise<AgentSnapshot> Promise<ManagedAgent>
> = new Map(); > = new Map();
constructor( constructor(
@@ -325,7 +325,7 @@ export class Session {
throw new Error(`Agent ${agentId} not found`); throw new Error(`Agent ${agentId} not found`);
} }
if (snapshot.status !== "running") { if (snapshot.lifecycle !== "running") {
return; return;
} }
@@ -532,10 +532,10 @@ export class Session {
} }
private async buildAgentPayload( private async buildAgentPayload(
agent: AgentSnapshot agent: ManagedAgent
): Promise<AgentSnapshotPayload> { ): Promise<AgentSnapshotPayload> {
const title = await this.getStoredAgentTitle(agent.id); const title = await this.getStoredAgentTitle(agent.id);
return serializeAgentSnapshot(agent, { title }); return toAgentPayload(agent, { title });
} }
private buildStoredAgentPayload(record: StoredAgentRecord): AgentSnapshotPayload { private buildStoredAgentPayload(record: StoredAgentRecord): AgentSnapshotPayload {
@@ -576,7 +576,7 @@ export class Session {
}; };
} }
private async ensureAgentLoaded(agentId: string): Promise<AgentSnapshot> { private async ensureAgentLoaded(agentId: string): Promise<ManagedAgent> {
const existing = this.agentManager.getAgent(agentId); const existing = this.agentManager.getAgent(agentId);
if (existing) { if (existing) {
return existing; return existing;
@@ -594,7 +594,7 @@ export class Session {
} }
const handle = toAgentPersistenceHandle(record.persistence); const handle = toAgentPersistenceHandle(record.persistence);
let snapshot: AgentSnapshot; let snapshot: ManagedAgent;
if (handle) { if (handle) {
snapshot = await this.agentManager.resumeAgent( snapshot = await this.agentManager.resumeAgent(
handle, handle,
@@ -619,7 +619,7 @@ export class Session {
} }
} }
private async forwardAgentState(agent: AgentSnapshot): Promise<void> { private async forwardAgentState(agent: ManagedAgent): Promise<void> {
try { try {
const payload = await this.buildAgentPayload(agent); const payload = await this.buildAgentPayload(agent);
this.emit({ this.emit({
@@ -1189,7 +1189,7 @@ export class Session {
payload: { payload: {
status: "agent_initialized", status: "agent_initialized",
agentId, agentId,
agentStatus: snapshot.status, agentStatus: snapshot.lifecycle,
requestId, requestId,
timelineSize, timelineSize,
}, },
@@ -1197,7 +1197,7 @@ export class Session {
} }
console.log( console.log(
`[Session ${this.clientId}] Agent ${agentId} initialized with ${timelineSize} timeline item(s); status=${snapshot.status}` `[Session ${this.clientId}] Agent ${agentId} initialized with ${timelineSize} timeline item(s); status=${snapshot.lifecycle}`
); );
} catch (error: any) { } catch (error: any) {
console.error( console.error(
@@ -1370,7 +1370,7 @@ export class Session {
); );
try { try {
let snapshot: AgentSnapshot; let snapshot: ManagedAgent;
const existing = this.agentManager.getAgent(agentId); const existing = this.agentManager.getAgent(agentId);
if (existing) { if (existing) {
await this.interruptAgentIfRunning(agentId); await this.interruptAgentIfRunning(agentId);
@@ -2048,7 +2048,7 @@ export class Session {
} }
} }
private emitAgentTimelineSnapshot(agent: AgentSnapshot): number { private emitAgentTimelineSnapshot(agent: ManagedAgent): number {
const timeline = this.agentManager.getTimeline(agent.id); const timeline = this.agentManager.getTimeline(agent.id);
const events = timeline.map((item) => ({ const events = timeline.map((item) => ({
event: serializeAgentStreamEvent({ event: serializeAgentStreamEvent({