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

View File

@@ -9,7 +9,7 @@ import type {
} from "./agent-sdk-types.js";
import type {
AgentManager,
AgentSnapshot,
ManagedAgent,
WaitForAgentResult,
} from "./agent-manager.js";
import {
@@ -19,6 +19,7 @@ import {
AgentSnapshotPayloadSchema,
serializeAgentSnapshot,
} from "../messages.js";
import { toAgentPayload } from "./agent-projections.js";
import { curateAgentActivity } from "./activity-curator.js";
import { AGENT_PROVIDER_DEFINITIONS } from "./provider-manifest.js";
import { AgentRegistry } from "./agent-registry.js";
@@ -90,7 +91,7 @@ async function resolveAgentTitle(
async function serializeSnapshotWithMetadata(
agentRegistry: AgentRegistry,
snapshot: AgentSnapshot
snapshot: ManagedAgent
) {
const title = await resolveAgentTitle(agentRegistry, snapshot.id);
return serializeAgentSnapshot(snapshot, { title });
@@ -232,7 +233,7 @@ export async function createAgentMcpServer(
structuredContent: ensureValidJson({
agentId: snapshot.id,
type: provider,
status: snapshot.status,
status: snapshot.lifecycle,
cwd: snapshot.cwd,
currentModeId: snapshot.currentModeId,
availableModes: snapshot.availableModes,
@@ -394,7 +395,7 @@ export async function createAgentMcpServer(
const responseData = {
success: true,
status: snapshot?.status ?? "idle",
status: snapshot?.lifecycle ?? "idle",
lastMessage: null,
permission: null,
};
@@ -435,7 +436,7 @@ export async function createAgentMcpServer(
return {
content: [],
structuredContent: ensureValidJson({
status: snapshot.status,
status: snapshot.lifecycle,
snapshot: structuredSnapshot,
}),
};
@@ -616,13 +617,14 @@ export async function createAgentMcpServer(
},
},
async () => {
const permissions = agentManager.listAgents().flatMap((agent) =>
agent.pendingPermissions.map((request) => ({
const permissions = agentManager.listAgents().flatMap((agent) => {
const payload = toAgentPayload(agent);
return payload.pendingPermissions.map((request) => ({
agentId: agent.id,
status: agent.status,
status: payload.status,
request,
}))
);
}));
});
return {
content: [],

View File

@@ -1,8 +1,9 @@
import { z } from "zod";
import {
AGENT_LIFECYCLE_STATUSES,
type AgentSnapshot,
type ManagedAgent,
} from "./agent/agent-manager.js";
import { toAgentPayload } from "./agent/agent-projections.js";
import type {
AgentCapabilityFlags,
AgentModelDefinition,
@@ -16,16 +17,6 @@ import type {
AgentUsage,
} 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 AgentProviderSchema = z.enum(AGENT_PROVIDERS);
@@ -230,96 +221,17 @@ export const AgentSnapshotPayloadSchema = z.object({
title: z.string().nullable(),
});
function sanitizeOptionalJson(value: unknown): unknown {
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 AgentSnapshotPayload = z.infer<typeof AgentSnapshotPayloadSchema>;
export type AgentStreamEventPayload = z.infer<
typeof AgentStreamEventPayloadSchema
>;
export function serializeAgentSnapshot(
snapshot: AgentSnapshot,
agent: ManagedAgent,
options?: { title?: string | null }
): AgentSnapshotPayload {
const payload: AgentSnapshotPayload = {
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;
return toAgentPayload(agent, options);
}
export function serializeAgentStreamEvent(

View File

@@ -12,7 +12,6 @@ import {
OpenRouterProviderOptions,
} from "@openrouter/ai-sdk-provider";
import {
serializeAgentSnapshot,
serializeAgentStreamEvent,
type AgentSnapshotPayload,
type SessionInboundMessage,
@@ -39,7 +38,8 @@ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { createTerminalMcpServer } from "./terminal-mcp/index.js";
import { fetchProviderModelCatalog } from "./agent/model-catalog.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 {
AgentPermissionResponse,
AgentPromptInput,
@@ -234,7 +234,7 @@ export class Session {
private unsubscribeAgentEvents: (() => void) | null = null;
private pendingAgentInitializations: Map<
string,
Promise<AgentSnapshot>
Promise<ManagedAgent>
> = new Map();
constructor(
@@ -325,7 +325,7 @@ export class Session {
throw new Error(`Agent ${agentId} not found`);
}
if (snapshot.status !== "running") {
if (snapshot.lifecycle !== "running") {
return;
}
@@ -532,10 +532,10 @@ export class Session {
}
private async buildAgentPayload(
agent: AgentSnapshot
agent: ManagedAgent
): Promise<AgentSnapshotPayload> {
const title = await this.getStoredAgentTitle(agent.id);
return serializeAgentSnapshot(agent, { title });
return toAgentPayload(agent, { title });
}
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);
if (existing) {
return existing;
@@ -594,7 +594,7 @@ export class Session {
}
const handle = toAgentPersistenceHandle(record.persistence);
let snapshot: AgentSnapshot;
let snapshot: ManagedAgent;
if (handle) {
snapshot = await this.agentManager.resumeAgent(
handle,
@@ -619,7 +619,7 @@ export class Session {
}
}
private async forwardAgentState(agent: AgentSnapshot): Promise<void> {
private async forwardAgentState(agent: ManagedAgent): Promise<void> {
try {
const payload = await this.buildAgentPayload(agent);
this.emit({
@@ -1189,7 +1189,7 @@ export class Session {
payload: {
status: "agent_initialized",
agentId,
agentStatus: snapshot.status,
agentStatus: snapshot.lifecycle,
requestId,
timelineSize,
},
@@ -1197,7 +1197,7 @@ export class Session {
}
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) {
console.error(
@@ -1370,7 +1370,7 @@ export class Session {
);
try {
let snapshot: AgentSnapshot;
let snapshot: ManagedAgent;
const existing = this.agentManager.getAgent(agentId);
if (existing) {
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 events = timeline.map((item) => ({
event: serializeAgentStreamEvent({