Merge branch 'main' of github.com:boudra/voice-dev

This commit is contained in:
Mohamed Boudra
2025-12-20 12:57:33 +07:00
16 changed files with 305 additions and 58 deletions
@@ -66,6 +66,14 @@ export type WaitForAgentResult = {
lastMessage: string | null;
};
type AttentionState =
| { requiresAttention: false }
| {
requiresAttention: true;
attentionReason: "finished" | "error" | "permission";
attentionTimestamp: Date;
};
type ManagedAgentBase = {
id: string;
provider: AgentProvider;
@@ -84,6 +92,7 @@ type ManagedAgentBase = {
sessionId: string | null;
lastUsage?: AgentUsage;
lastError?: string;
attention: AttentionState;
};
type ManagedAgentWithSession = ManagedAgentBase & {
@@ -166,6 +175,7 @@ export class AgentManager {
private readonly maxTimelineItems: number;
private readonly idFactory: () => string;
private readonly registry?: AgentRegistry;
private readonly previousStatuses = new Map<string, AgentLifecycleStatus>();
constructor(options?: AgentManagerOptions) {
this.maxTimelineItems =
@@ -343,6 +353,8 @@ export class AgentManager {
async closeAgent(agentId: string): Promise<void> {
const agent = this.requireAgent(agentId);
this.agents.delete(agentId);
// Clean up previousStatus to prevent memory leak
this.previousStatuses.delete(agentId);
const session = agent.session;
const closedAgent: ManagedAgent = {
...agent,
@@ -362,6 +374,15 @@ export class AgentManager {
this.emitState(agent);
}
async clearAgentAttention(agentId: string): Promise<void> {
const agent = this.requireAgent(agentId);
if (agent.attention.requiresAttention) {
agent.attention = { requiresAttention: false };
await this.persistSnapshot(agent);
this.emitState(agent);
}
}
async runAgent(
agentId: string,
prompt: AgentPromptInput,
@@ -702,9 +723,12 @@ export class AgentManager {
persistence: session.describePersistence(),
historyPrimed: false,
lastUserMessageAt: null,
attention: { requiresAttention: false },
} as ActiveManagedAgent;
this.agents.set(agentId, managed);
// Initialize previousStatus to track transitions
this.previousStatuses.set(agentId, managed.lifecycle);
await this.persistSnapshot(managed, {
title: config.title ?? null,
});
@@ -820,12 +844,76 @@ export class AgentManager {
}
private emitState(agent: ManagedAgent): void {
// Check if attention should be set based on status change
this.checkAndSetAttention(agent);
this.dispatch({
type: "agent_state",
agent: { ...agent },
});
}
private checkAndSetAttention(agent: ManagedAgent): void {
const previousStatus = this.previousStatuses.get(agent.id);
const currentStatus = agent.lifecycle;
// Track the new status
this.previousStatuses.set(agent.id, currentStatus);
// Skip if already requires attention
if (agent.attention.requiresAttention) {
return;
}
// Check if agent transitioned from running to idle (finished)
if (previousStatus === "running" && currentStatus === "idle") {
agent.attention = {
requiresAttention: true,
attentionReason: "finished",
attentionTimestamp: new Date(),
};
this.broadcastAgentAttention(agent, "finished");
void this.persistSnapshot(agent);
return;
}
// Check if agent entered error state
if (currentStatus === "error") {
agent.attention = {
requiresAttention: true,
attentionReason: "error",
attentionTimestamp: new Date(),
};
this.broadcastAgentAttention(agent, "error");
void this.persistSnapshot(agent);
return;
}
// Check if agent has pending permissions
if (agent.pendingPermissions.size > 0) {
agent.attention = {
requiresAttention: true,
attentionReason: "permission",
attentionTimestamp: new Date(),
};
this.broadcastAgentAttention(agent, "permission");
void this.persistSnapshot(agent);
return;
}
}
private broadcastAgentAttention(
agent: ManagedAgent,
reason: "finished" | "error" | "permission"
): void {
this.dispatchStream(agent.id, {
type: "attention_required",
provider: agent.provider,
reason,
timestamp: new Date().toISOString(),
});
}
private dispatchStream(agentId: string, event: AgentStreamEvent): void {
this.dispatch({ type: "agent_stream", agentId, event });
}
@@ -43,6 +43,13 @@ export function toStoredAgentRecord(
lastModeId: agent.currentModeId ?? config?.modeId ?? null,
config: config ?? null,
persistence,
requiresAttention: agent.attention.requiresAttention,
attentionReason: agent.attention.requiresAttention
? agent.attention.attentionReason
: null,
attentionTimestamp: agent.attention.requiresAttention
? agent.attention.attentionTimestamp.toISOString()
: null,
} satisfies StoredAgentRecord;
}
@@ -79,6 +86,16 @@ export function toAgentPayload(
payload.lastError = agent.lastError;
}
// Handle attention state
payload.requiresAttention = agent.attention.requiresAttention;
if (agent.attention.requiresAttention) {
payload.attentionReason = agent.attention.attentionReason;
payload.attentionTimestamp = agent.attention.attentionTimestamp.toISOString();
} else {
payload.attentionReason = null;
payload.attentionTimestamp = null;
}
return payload;
}
@@ -41,6 +41,9 @@ const STORED_AGENT_SCHEMA = z.object({
lastModeId: z.string().nullable().optional(),
config: SERIALIZABLE_CONFIG_SCHEMA,
persistence: PERSISTENCE_HANDLE_SCHEMA,
requiresAttention: z.boolean().optional(),
attentionReason: z.enum(["finished", "error", "permission"]).nullable().optional(),
attentionTimestamp: z.string().nullable().optional(),
});
export type SerializableAgentConfig = Pick<
@@ -80,6 +80,12 @@ export type AgentStreamEvent =
provider: AgentProvider;
requestId: string;
resolution: AgentPermissionResponse;
}
| {
type: "attention_required";
provider: AgentProvider;
reason: "finished" | "error" | "permission";
timestamp: string;
};
export type AgentPermissionRequestKind = "tool" | "plan" | "mode" | "other";
+16
View File
@@ -196,6 +196,12 @@ export const AgentStreamEventPayloadSchema = z.discriminatedUnion("type", [
requestId: z.string(),
resolution: AgentPermissionResponseSchema,
}),
z.object({
type: z.literal("attention_required"),
provider: AgentProviderSchema,
reason: z.enum(["finished", "error", "permission"]),
timestamp: z.string(),
}),
]);
const AgentPersistenceHandleSchema: z.ZodType<AgentPersistenceHandle | null> =
@@ -226,6 +232,9 @@ export const AgentSnapshotPayloadSchema = z.object({
lastUsage: AgentUsageSchema.optional(),
lastError: z.string().optional(),
title: z.string().nullable(),
requiresAttention: z.boolean().optional(),
attentionReason: z.enum(["finished", "error", "permission"]).nullable().optional(),
attentionTimestamp: z.string().nullable().optional(),
});
export type AgentSnapshotPayload = z.infer<typeof AgentSnapshotPayloadSchema>;
@@ -448,6 +457,11 @@ export const FileExplorerRequestSchema = z.object({
mode: z.enum(["list", "file"]),
});
export const ClearAgentAttentionMessageSchema = z.object({
type: z.literal("clear_agent_attention"),
agentId: z.string(),
});
export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
UserTextMessageSchema,
RealtimeAudioChunkMessageSchema,
@@ -473,6 +487,7 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
FileExplorerRequestSchema,
ListPersistedAgentsRequestMessageSchema,
GitRepoInfoRequestMessageSchema,
ClearAgentAttentionMessageSchema,
]);
export type SessionInboundMessage = z.infer<typeof SessionInboundMessageSchema>;
@@ -811,6 +826,7 @@ export type GitDiffResponse = z.infer<typeof GitDiffResponseSchema>;
export type FileExplorerRequest = z.infer<typeof FileExplorerRequestSchema>;
export type FileExplorerResponse = z.infer<typeof FileExplorerResponseSchema>;
export type RestartServerRequestMessage = z.infer<typeof RestartServerRequestMessageSchema>;
export type ClearAgentAttentionMessage = z.infer<typeof ClearAgentAttentionMessageSchema>;
// ============================================================================
// WebSocket Level Messages (wraps session messages)
+23
View File
@@ -837,6 +837,10 @@ export class Session {
case "list_provider_models_request":
await this.handleListProviderModelsRequest(msg);
break;
case "clear_agent_attention":
await this.handleClearAgentAttention(msg.agentId);
break;
}
} catch (error: any) {
console.error(
@@ -1844,6 +1848,25 @@ export class Session {
}
}
/**
* Handle clearing agent attention flag
*/
private async handleClearAgentAttention(agentId: string): Promise<void> {
console.log(
`[Session ${this.clientId}] Clearing attention for agent ${agentId}`
);
try {
await this.agentManager.clearAgentAttention(agentId);
} catch (error: any) {
console.error(
`[Session ${this.clientId}] Failed to clear agent attention:`,
error
);
// Don't throw - this is not critical
}
}
/**
* Handle agent permission response from user
*/