refactor: convert ManagedAgent to discriminated unions and remove AgentSnapshot
Replace ManagedAgent with discriminated union keyed on lifecycle state to make impossible states unrepresentable. Remove AgentSnapshot type and toSnapshot method entirely - consumers now receive ManagedAgent directly. - Define ManagedAgent as discriminated union with 5 lifecycle states - Add comprehensive immutability protection (deep clone + freeze) - Update getAgent/listAgents to return immutable ManagedAgent views - Remove AgentSnapshot type and toSnapshot method - Update event emissions to use ManagedAgent Breaking change: All consumers must now use ManagedAgent or projection functions instead of AgentSnapshot. Tasks 2 & 3 of 8 in agent architecture refactor. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -33,27 +33,8 @@ export const AGENT_LIFECYCLE_STATUSES = [
|
|||||||
export type AgentLifecycleStatus =
|
export type AgentLifecycleStatus =
|
||||||
(typeof AGENT_LIFECYCLE_STATUSES)[number];
|
(typeof AGENT_LIFECYCLE_STATUSES)[number];
|
||||||
|
|
||||||
export type AgentSnapshot = {
|
|
||||||
id: string;
|
|
||||||
provider: AgentProvider;
|
|
||||||
cwd: string;
|
|
||||||
model: string | null;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
lastUserMessageAt: Date | null;
|
|
||||||
status: AgentLifecycleStatus;
|
|
||||||
sessionId: string | null;
|
|
||||||
capabilities: AgentCapabilityFlags;
|
|
||||||
currentModeId: string | null;
|
|
||||||
availableModes: AgentMode[];
|
|
||||||
pendingPermissions: AgentPermissionRequest[];
|
|
||||||
persistence: AgentPersistenceHandle | null;
|
|
||||||
lastUsage?: AgentUsage;
|
|
||||||
lastError?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AgentManagerEvent =
|
export type AgentManagerEvent =
|
||||||
| { type: "agent_state"; agent: AgentSnapshot }
|
| { type: "agent_state"; agent: ManagedAgent }
|
||||||
| { type: "agent_stream"; agentId: string; event: AgentStreamEvent };
|
| { type: "agent_stream"; agentId: string; event: AgentStreamEvent };
|
||||||
|
|
||||||
export type AgentSubscriber = (event: AgentManagerEvent) => void;
|
export type AgentSubscriber = (event: AgentManagerEvent) => void;
|
||||||
@@ -83,29 +64,70 @@ export type WaitForAgentResult = {
|
|||||||
lastMessage: string | null;
|
lastMessage: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ManagedAgent = {
|
type ManagedAgentBase = {
|
||||||
id: string;
|
id: string;
|
||||||
provider: AgentProvider;
|
provider: AgentProvider;
|
||||||
cwd: string;
|
cwd: string;
|
||||||
session: AgentSession;
|
|
||||||
sessionId: string | null;
|
|
||||||
capabilities: AgentCapabilityFlags;
|
capabilities: AgentCapabilityFlags;
|
||||||
config: AgentSessionConfig;
|
config: AgentSessionConfig;
|
||||||
status: AgentLifecycleStatus;
|
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
availableModes: AgentMode[];
|
availableModes: AgentMode[];
|
||||||
currentModeId: string | null;
|
currentModeId: string | null;
|
||||||
pendingPermissions: Map<string, AgentPermissionRequest>;
|
pendingPermissions: Map<string, AgentPermissionRequest>;
|
||||||
pendingRun: AsyncGenerator<AgentStreamEvent> | null;
|
|
||||||
timeline: AgentTimelineItem[];
|
timeline: AgentTimelineItem[];
|
||||||
persistence: AgentPersistenceHandle | null;
|
persistence: AgentPersistenceHandle | null;
|
||||||
lastUsage?: AgentUsage;
|
|
||||||
lastError?: string;
|
|
||||||
historyPrimed: boolean;
|
historyPrimed: boolean;
|
||||||
lastUserMessageAt: Date | null;
|
lastUserMessageAt: Date | null;
|
||||||
|
sessionId: string | null;
|
||||||
|
lastUsage?: AgentUsage;
|
||||||
|
lastError?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ManagedAgentWithSession = ManagedAgentBase & {
|
||||||
|
session: AgentSession;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ManagedAgentInitializing = ManagedAgentWithSession & {
|
||||||
|
lifecycle: "initializing";
|
||||||
|
pendingRun: null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ManagedAgentIdle = ManagedAgentWithSession & {
|
||||||
|
lifecycle: "idle";
|
||||||
|
pendingRun: null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ManagedAgentRunning = ManagedAgentWithSession & {
|
||||||
|
lifecycle: "running";
|
||||||
|
pendingRun: AsyncGenerator<AgentStreamEvent>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ManagedAgentError = ManagedAgentWithSession & {
|
||||||
|
lifecycle: "error";
|
||||||
|
pendingRun: null;
|
||||||
|
lastError: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ManagedAgentClosed = ManagedAgentBase & {
|
||||||
|
lifecycle: "closed";
|
||||||
|
session: null;
|
||||||
|
pendingRun: null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ManagedAgent =
|
||||||
|
| ManagedAgentInitializing
|
||||||
|
| ManagedAgentIdle
|
||||||
|
| ManagedAgentRunning
|
||||||
|
| ManagedAgentError
|
||||||
|
| ManagedAgentClosed;
|
||||||
|
|
||||||
|
type ActiveManagedAgent =
|
||||||
|
| ManagedAgentInitializing
|
||||||
|
| ManagedAgentIdle
|
||||||
|
| ManagedAgentRunning
|
||||||
|
| ManagedAgentError;
|
||||||
|
|
||||||
type SubscriptionRecord = {
|
type SubscriptionRecord = {
|
||||||
callback: AgentSubscriber;
|
callback: AgentSubscriber;
|
||||||
agentId: string | null;
|
agentId: string | null;
|
||||||
@@ -117,6 +139,23 @@ const BUSY_STATUSES: AgentLifecycleStatus[] = [
|
|||||||
"running",
|
"running",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const READ_ONLY_AGENT_ERROR_MESSAGE =
|
||||||
|
"ManagedAgent views returned by AgentManager are read-only";
|
||||||
|
|
||||||
|
class ReadonlyPendingPermissionsMap<K, V> extends Map<K, V> {
|
||||||
|
override set(): this {
|
||||||
|
throw new Error(READ_ONLY_AGENT_ERROR_MESSAGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
override delete(): boolean {
|
||||||
|
throw new Error(READ_ONLY_AGENT_ERROR_MESSAGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
override clear(): void {
|
||||||
|
throw new Error(READ_ONLY_AGENT_ERROR_MESSAGE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function isAgentBusy(status: AgentLifecycleStatus): boolean {
|
function isAgentBusy(status: AgentLifecycleStatus): boolean {
|
||||||
return BUSY_STATUSES.includes(status);
|
return BUSY_STATUSES.includes(status);
|
||||||
}
|
}
|
||||||
@@ -137,7 +176,7 @@ function createAbortError(
|
|||||||
|
|
||||||
export class AgentManager {
|
export class AgentManager {
|
||||||
private readonly clients = new Map<AgentProvider, AgentClient>();
|
private readonly clients = new Map<AgentProvider, AgentClient>();
|
||||||
private readonly agents = new Map<string, ManagedAgent>();
|
private readonly agents = new Map<string, ActiveManagedAgent>();
|
||||||
private readonly subscribers = new Set<SubscriptionRecord>();
|
private readonly subscribers = new Set<SubscriptionRecord>();
|
||||||
private readonly maxTimelineItems: number;
|
private readonly maxTimelineItems: number;
|
||||||
private readonly idFactory: () => string;
|
private readonly idFactory: () => string;
|
||||||
@@ -170,11 +209,17 @@ export class AgentManager {
|
|||||||
if (record.agentId) {
|
if (record.agentId) {
|
||||||
const agent = this.agents.get(record.agentId);
|
const agent = this.agents.get(record.agentId);
|
||||||
if (agent) {
|
if (agent) {
|
||||||
callback({ type: "agent_state", agent: this.toSnapshot(agent) });
|
callback({
|
||||||
|
type: "agent_state",
|
||||||
|
agent: this.createAgentView(agent),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (const agent of this.agents.values()) {
|
for (const agent of this.agents.values()) {
|
||||||
callback({ type: "agent_state", agent: this.toSnapshot(agent) });
|
callback({
|
||||||
|
type: "agent_state",
|
||||||
|
agent: this.createAgentView(agent),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -184,9 +229,9 @@ export class AgentManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
listAgents(): AgentSnapshot[] {
|
listAgents(): ManagedAgent[] {
|
||||||
return Array.from(this.agents.values()).map((agent) =>
|
return Array.from(this.agents.values()).map((agent) =>
|
||||||
this.toSnapshot(agent)
|
this.createAgentView(agent)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,9 +273,9 @@ export class AgentManager {
|
|||||||
.slice(0, limit);
|
.slice(0, limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
getAgent(id: string): AgentSnapshot | null {
|
getAgent(id: string): ManagedAgent | null {
|
||||||
const agent = this.agents.get(id);
|
const agent = this.agents.get(id);
|
||||||
return agent ? this.toSnapshot(agent) : null;
|
return agent ? this.createAgentView(agent) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
getTimeline(id: string): AgentTimelineItem[] {
|
getTimeline(id: string): AgentTimelineItem[] {
|
||||||
@@ -241,7 +286,7 @@ export class AgentManager {
|
|||||||
async createAgent(
|
async createAgent(
|
||||||
config: AgentSessionConfig,
|
config: AgentSessionConfig,
|
||||||
agentId?: string
|
agentId?: string
|
||||||
): Promise<AgentSnapshot> {
|
): Promise<ManagedAgent> {
|
||||||
const normalizedConfig = await this.normalizeConfig(config);
|
const normalizedConfig = await this.normalizeConfig(config);
|
||||||
const client = this.requireClient(normalizedConfig.provider);
|
const client = this.requireClient(normalizedConfig.provider);
|
||||||
const session = await client.createSession(normalizedConfig);
|
const session = await client.createSession(normalizedConfig);
|
||||||
@@ -256,7 +301,7 @@ export class AgentManager {
|
|||||||
handle: AgentPersistenceHandle,
|
handle: AgentPersistenceHandle,
|
||||||
overrides?: Partial<AgentSessionConfig>,
|
overrides?: Partial<AgentSessionConfig>,
|
||||||
agentId?: string
|
agentId?: string
|
||||||
): Promise<AgentSnapshot> {
|
): Promise<ManagedAgent> {
|
||||||
const metadata = (handle.metadata ?? {}) as Partial<AgentSessionConfig>;
|
const metadata = (handle.metadata ?? {}) as Partial<AgentSessionConfig>;
|
||||||
const mergedConfig = {
|
const mergedConfig = {
|
||||||
...metadata,
|
...metadata,
|
||||||
@@ -277,7 +322,7 @@ export class AgentManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async refreshAgentFromPersistence(agentId: string): Promise<AgentSnapshot> {
|
async refreshAgentFromPersistence(agentId: string): Promise<ManagedAgent> {
|
||||||
const existing = this.requireAgent(agentId);
|
const existing = this.requireAgent(agentId);
|
||||||
const handle = existing.persistence;
|
const handle = existing.persistence;
|
||||||
if (!handle) {
|
if (!handle) {
|
||||||
@@ -311,9 +356,16 @@ export class AgentManager {
|
|||||||
async closeAgent(agentId: string): Promise<void> {
|
async closeAgent(agentId: string): Promise<void> {
|
||||||
const agent = this.requireAgent(agentId);
|
const agent = this.requireAgent(agentId);
|
||||||
this.agents.delete(agentId);
|
this.agents.delete(agentId);
|
||||||
agent.status = "closed";
|
const session = agent.session;
|
||||||
await agent.session.close();
|
const closedAgent: ManagedAgent = {
|
||||||
this.emitState(agent);
|
...agent,
|
||||||
|
lifecycle: "closed",
|
||||||
|
session: null,
|
||||||
|
pendingRun: null,
|
||||||
|
sessionId: agent.sessionId,
|
||||||
|
};
|
||||||
|
await session.close();
|
||||||
|
this.emitState(closedAgent);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setAgentMode(agentId: string, modeId: string): Promise<void> {
|
async setAgentMode(agentId: string, modeId: string): Promise<void> {
|
||||||
@@ -382,16 +434,14 @@ export class AgentManager {
|
|||||||
prompt: AgentPromptInput,
|
prompt: AgentPromptInput,
|
||||||
options?: AgentRunOptions
|
options?: AgentRunOptions
|
||||||
): AsyncGenerator<AgentStreamEvent> {
|
): AsyncGenerator<AgentStreamEvent> {
|
||||||
const agent = this.requireAgent(agentId);
|
const existingAgent = this.requireAgent(agentId);
|
||||||
if (agent.status === "closed") {
|
if (existingAgent.pendingRun) {
|
||||||
throw new Error(`Agent ${agentId} is closed`);
|
|
||||||
}
|
|
||||||
if (agent.pendingRun) {
|
|
||||||
throw new Error(`Agent ${agentId} already has an active run`);
|
throw new Error(`Agent ${agentId} already has an active run`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const agent = existingAgent as ActiveManagedAgent;
|
||||||
const iterator = agent.session.stream(prompt, options);
|
const iterator = agent.session.stream(prompt, options);
|
||||||
agent.status = "running";
|
agent.lifecycle = "running";
|
||||||
agent.pendingRun = iterator;
|
agent.pendingRun = iterator;
|
||||||
agent.lastError = undefined;
|
agent.lastError = undefined;
|
||||||
this.emitState(agent);
|
this.emitState(agent);
|
||||||
@@ -410,11 +460,12 @@ export class AgentManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
agent.pendingRun = null;
|
const mutableAgent = agent as ActiveManagedAgent;
|
||||||
agent.status = error ? "error" : "idle";
|
mutableAgent.pendingRun = null;
|
||||||
agent.lastError = error;
|
mutableAgent.lifecycle = error ? "error" : "idle";
|
||||||
agent.persistence = agent.session.describePersistence();
|
mutableAgent.lastError = error;
|
||||||
this.emitState(agent);
|
mutableAgent.persistence = mutableAgent.session.describePersistence();
|
||||||
|
this.emitState(mutableAgent);
|
||||||
};
|
};
|
||||||
|
|
||||||
const self = this;
|
const self = this;
|
||||||
@@ -491,6 +542,11 @@ export class AgentManager {
|
|||||||
return Array.from(agent.pendingPermissions.values());
|
return Array.from(agent.pendingPermissions.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private peekPendingPermission(agent: ManagedAgent): AgentPermissionRequest | null {
|
||||||
|
const iterator = agent.pendingPermissions.values().next();
|
||||||
|
return iterator.done ? null : iterator.value;
|
||||||
|
}
|
||||||
|
|
||||||
async primeAgentHistory(agentId: string): Promise<void> {
|
async primeAgentHistory(agentId: string): Promise<void> {
|
||||||
const agent = this.requireAgent(agentId);
|
const agent = this.requireAgent(agentId);
|
||||||
await this.primeHistory(agent);
|
await this.primeHistory(agent);
|
||||||
@@ -532,18 +588,18 @@ export class AgentManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const immediatePermission = snapshot.pendingPermissions[0] ?? null;
|
const immediatePermission = this.peekPendingPermission(snapshot);
|
||||||
if (immediatePermission) {
|
if (immediatePermission) {
|
||||||
return {
|
return {
|
||||||
status: snapshot.status,
|
status: snapshot.lifecycle,
|
||||||
permission: immediatePermission,
|
permission: immediatePermission,
|
||||||
lastMessage: this.getLastAssistantMessage(agentId)
|
lastMessage: this.getLastAssistantMessage(agentId)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isAgentBusy(snapshot.status)) {
|
if (!isAgentBusy(snapshot.lifecycle)) {
|
||||||
return {
|
return {
|
||||||
status: snapshot.status,
|
status: snapshot.lifecycle,
|
||||||
permission: null,
|
permission: null,
|
||||||
lastMessage: this.getLastAssistantMessage(agentId)
|
lastMessage: this.getLastAssistantMessage(agentId)
|
||||||
};
|
};
|
||||||
@@ -562,7 +618,7 @@ export class AgentManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentStatus: AgentLifecycleStatus = snapshot.status;
|
let currentStatus: AgentLifecycleStatus = snapshot.lifecycle;
|
||||||
|
|
||||||
// Bug #3 Fix: Declare unsubscribe and abortHandler upfront so cleanup can reference them
|
// Bug #3 Fix: Declare unsubscribe and abortHandler upfront so cleanup can reference them
|
||||||
let unsubscribe: (() => void) | null = null;
|
let unsubscribe: (() => void) | null = null;
|
||||||
@@ -615,13 +671,13 @@ export class AgentManager {
|
|||||||
(event) => {
|
(event) => {
|
||||||
// Bug #2 Fix: Only handle agent_state events, remove redundant agent_stream handling
|
// Bug #2 Fix: Only handle agent_state events, remove redundant agent_stream handling
|
||||||
if (event.type === "agent_state") {
|
if (event.type === "agent_state") {
|
||||||
currentStatus = event.agent.status;
|
currentStatus = event.agent.lifecycle;
|
||||||
const pending = event.agent.pendingPermissions[0] ?? null;
|
const pending = this.peekPendingPermission(event.agent);
|
||||||
if (pending) {
|
if (pending) {
|
||||||
finish(pending);
|
finish(pending);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!isAgentBusy(event.agent.status)) {
|
if (!isAgentBusy(event.agent.lifecycle)) {
|
||||||
finish(null);
|
finish(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -635,12 +691,12 @@ export class AgentManager {
|
|||||||
session: AgentSession,
|
session: AgentSession,
|
||||||
config: AgentSessionConfig,
|
config: AgentSessionConfig,
|
||||||
agentId: string
|
agentId: string
|
||||||
): Promise<AgentSnapshot> {
|
): Promise<ManagedAgent> {
|
||||||
if (this.agents.has(agentId)) {
|
if (this.agents.has(agentId)) {
|
||||||
throw new Error(`Agent with id ${agentId} already exists`);
|
throw new Error(`Agent with id ${agentId} already exists`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const managed: ManagedAgent = {
|
const managed = {
|
||||||
id: agentId,
|
id: agentId,
|
||||||
provider: config.provider,
|
provider: config.provider,
|
||||||
cwd: config.cwd,
|
cwd: config.cwd,
|
||||||
@@ -648,7 +704,7 @@ export class AgentManager {
|
|||||||
sessionId: session.id,
|
sessionId: session.id,
|
||||||
capabilities: session.capabilities,
|
capabilities: session.capabilities,
|
||||||
config,
|
config,
|
||||||
status: "initializing",
|
lifecycle: "initializing",
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
availableModes: [],
|
availableModes: [],
|
||||||
@@ -659,18 +715,18 @@ export class AgentManager {
|
|||||||
persistence: session.describePersistence(),
|
persistence: session.describePersistence(),
|
||||||
historyPrimed: false,
|
historyPrimed: false,
|
||||||
lastUserMessageAt: null,
|
lastUserMessageAt: null,
|
||||||
};
|
} as ActiveManagedAgent;
|
||||||
|
|
||||||
this.agents.set(agentId, managed);
|
this.agents.set(agentId, managed);
|
||||||
this.emitState(managed);
|
this.emitState(managed);
|
||||||
|
|
||||||
await this.refreshSessionState(managed);
|
await this.refreshSessionState(managed);
|
||||||
managed.status = "idle";
|
managed.lifecycle = "idle";
|
||||||
this.emitState(managed);
|
this.emitState(managed);
|
||||||
return this.toSnapshot(managed);
|
return this.createAgentView(managed);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async refreshSessionState(agent: ManagedAgent): Promise<void> {
|
private async refreshSessionState(agent: ActiveManagedAgent): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const modes = await agent.session.getAvailableModes();
|
const modes = await agent.session.getAvailableModes();
|
||||||
agent.availableModes = modes;
|
agent.availableModes = modes;
|
||||||
@@ -694,7 +750,7 @@ export class AgentManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async primeHistory(agent: ManagedAgent): Promise<void> {
|
private async primeHistory(agent: ActiveManagedAgent): Promise<void> {
|
||||||
if (agent.historyPrimed) {
|
if (agent.historyPrimed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -709,7 +765,7 @@ export class AgentManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private handleStreamEvent(
|
private handleStreamEvent(
|
||||||
agent: ManagedAgent,
|
agent: ActiveManagedAgent,
|
||||||
event: AgentStreamEvent,
|
event: AgentStreamEvent,
|
||||||
options?: { fromHistory?: boolean }
|
options?: { fromHistory?: boolean }
|
||||||
): void {
|
): void {
|
||||||
@@ -763,7 +819,10 @@ export class AgentManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private emitState(agent: ManagedAgent): void {
|
private emitState(agent: ManagedAgent): void {
|
||||||
this.dispatch({ type: "agent_state", agent: this.toSnapshot(agent) });
|
this.dispatch({
|
||||||
|
type: "agent_state",
|
||||||
|
agent: this.createAgentView(agent),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private dispatchStream(agentId: string, event: AgentStreamEvent): void {
|
private dispatchStream(agentId: string, event: AgentStreamEvent): void {
|
||||||
@@ -790,25 +849,156 @@ export class AgentManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private toSnapshot(agent: ManagedAgent): AgentSnapshot {
|
private createAgentView(agent: ManagedAgent): ManagedAgent {
|
||||||
|
const clone = this.cloneAgent(agent);
|
||||||
|
clone.pendingPermissions = this.createReadonlyPendingPermissionsMap(
|
||||||
|
clone.pendingPermissions
|
||||||
|
);
|
||||||
|
|
||||||
|
this.deepFreezeValue(clone.availableModes);
|
||||||
|
this.deepFreezeValue(clone.timeline);
|
||||||
|
this.deepFreezeValue(clone.capabilities);
|
||||||
|
this.deepFreezeValue(clone.config);
|
||||||
|
if (clone.persistence) {
|
||||||
|
this.deepFreezeValue(clone.persistence);
|
||||||
|
}
|
||||||
|
if (clone.lastUsage) {
|
||||||
|
this.deepFreezeValue(clone.lastUsage);
|
||||||
|
}
|
||||||
|
if (clone.lastUserMessageAt) {
|
||||||
|
Object.freeze(clone.lastUserMessageAt);
|
||||||
|
}
|
||||||
|
Object.freeze(clone.createdAt);
|
||||||
|
Object.freeze(clone.updatedAt);
|
||||||
|
|
||||||
|
return Object.freeze(clone) as ManagedAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private cloneAgent(agent: ManagedAgent): ManagedAgent {
|
||||||
return {
|
return {
|
||||||
id: agent.id,
|
...agent,
|
||||||
provider: agent.provider,
|
createdAt: new Date(agent.createdAt.getTime()),
|
||||||
cwd: agent.cwd,
|
updatedAt: new Date(agent.updatedAt.getTime()),
|
||||||
model: agent.config.model ?? null,
|
lastUserMessageAt: agent.lastUserMessageAt
|
||||||
createdAt: agent.createdAt,
|
? new Date(agent.lastUserMessageAt.getTime())
|
||||||
updatedAt: agent.updatedAt,
|
: null,
|
||||||
lastUserMessageAt: agent.lastUserMessageAt,
|
pendingPermissions: new Map(
|
||||||
status: agent.status,
|
Array.from(agent.pendingPermissions.entries()).map(
|
||||||
sessionId: agent.sessionId,
|
([key, value]) => [key, this.cloneValue(value)]
|
||||||
capabilities: agent.capabilities,
|
)
|
||||||
currentModeId: agent.currentModeId,
|
),
|
||||||
availableModes: agent.availableModes,
|
availableModes: this.cloneValue(agent.availableModes),
|
||||||
pendingPermissions: Array.from(agent.pendingPermissions.values()),
|
timeline: this.cloneValue(agent.timeline),
|
||||||
persistence: agent.persistence,
|
capabilities: this.cloneValue(agent.capabilities),
|
||||||
lastUsage: agent.lastUsage,
|
config: this.cloneValue(agent.config),
|
||||||
lastError: agent.lastError,
|
persistence: agent.persistence
|
||||||
};
|
? this.cloneValue(agent.persistence)
|
||||||
|
: agent.persistence,
|
||||||
|
lastUsage: agent.lastUsage ? this.cloneValue(agent.lastUsage) : undefined,
|
||||||
|
} as ManagedAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private createReadonlyPendingPermissionsMap(
|
||||||
|
source: Map<string, AgentPermissionRequest>
|
||||||
|
): Map<string, AgentPermissionRequest> {
|
||||||
|
const readonlyMap = new ReadonlyPendingPermissionsMap(source);
|
||||||
|
for (const request of readonlyMap.values()) {
|
||||||
|
this.deepFreezeValue(request);
|
||||||
|
}
|
||||||
|
return Object.freeze(readonlyMap) as Map<string, AgentPermissionRequest>;
|
||||||
|
}
|
||||||
|
|
||||||
|
private cloneValue<T>(value: T): T {
|
||||||
|
if (value === null || typeof value !== "object") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (value instanceof Date) {
|
||||||
|
return new Date(value.getTime()) as T;
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map((item) => this.cloneValue(item)) as T;
|
||||||
|
}
|
||||||
|
if (value instanceof Map) {
|
||||||
|
const clone = new Map();
|
||||||
|
for (const [key, entryValue] of value.entries()) {
|
||||||
|
clone.set(this.cloneValue(key), this.cloneValue(entryValue));
|
||||||
|
}
|
||||||
|
return clone as T;
|
||||||
|
}
|
||||||
|
if (value instanceof Set) {
|
||||||
|
const clone = new Set();
|
||||||
|
for (const entryValue of value.values()) {
|
||||||
|
clone.add(this.cloneValue(entryValue));
|
||||||
|
}
|
||||||
|
return clone as T;
|
||||||
|
}
|
||||||
|
if (!this.isPlainObject(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
const clonedObject: Record<string, unknown> = {};
|
||||||
|
for (const [key, entryValue] of Object.entries(value)) {
|
||||||
|
clonedObject[key] = this.cloneValue(entryValue);
|
||||||
|
}
|
||||||
|
return clonedObject as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
private deepFreezeValue<T>(value: T, seen = new WeakSet<object>()): T {
|
||||||
|
if (value === null || typeof value !== "object") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (value instanceof Date) {
|
||||||
|
Object.freeze(value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const objectValue = value as object;
|
||||||
|
if (seen.has(objectValue)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
seen.add(objectValue);
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
for (const item of value) {
|
||||||
|
this.deepFreezeValue(item, seen);
|
||||||
|
}
|
||||||
|
Object.freeze(value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value instanceof Map) {
|
||||||
|
for (const mapValue of value.values()) {
|
||||||
|
this.deepFreezeValue(mapValue, seen);
|
||||||
|
}
|
||||||
|
Object.freeze(value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value instanceof Set) {
|
||||||
|
for (const setValue of value.values()) {
|
||||||
|
this.deepFreezeValue(setValue, seen);
|
||||||
|
}
|
||||||
|
Object.freeze(value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.isPlainObject(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const record = value as Record<string, unknown>;
|
||||||
|
for (const key of Object.keys(record)) {
|
||||||
|
this.deepFreezeValue(record[key], seen);
|
||||||
|
}
|
||||||
|
Object.freeze(record);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||||
|
if (value === null || typeof value !== "object") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const prototype = Object.getPrototypeOf(value);
|
||||||
|
return prototype === Object.prototype || prototype === null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async normalizeConfig(
|
private async normalizeConfig(
|
||||||
@@ -845,7 +1035,7 @@ export class AgentManager {
|
|||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
private requireAgent(id: string): ManagedAgent {
|
private requireAgent(id: string): ActiveManagedAgent {
|
||||||
const agent = this.agents.get(id);
|
const agent = this.agents.get(id);
|
||||||
if (!agent) {
|
if (!agent) {
|
||||||
throw new Error(`Unknown agent '${id}'`);
|
throw new Error(`Unknown agent '${id}'`);
|
||||||
|
|||||||
Reference in New Issue
Block a user