From 0893a9dc3313b6dbb9abdb69521953a53b353573 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 23 Oct 2025 09:01:38 +0200 Subject: [PATCH] ``` refactor: improve agent notification types and fix activity curator - Convert agent notifications to discriminated union (session/permission/status) - Fix activity curator to properly handle typed session updates - Serialize Date objects in AgentInfo for JSON compatibility - Use activity curator for title generation context - Reduce console.error to console.warn for non-critical WebSocket errors - Improve spacing in agent stream views - Delay title generation until 15+ updates available ``` --- packages/app/src/app/index.tsx | 1 + .../app/src/components/agent-stream-view.tsx | 2 +- packages/app/src/hooks/use-websocket.ts | 4 +- .../server/src/server/acp/activity-curator.ts | 57 ++-- .../server/src/server/acp/agent-manager.ts | 111 ++++--- packages/server/src/server/acp/mcp-server.ts | 25 +- packages/server/src/server/acp/types.ts | 16 +- packages/server/src/server/messages.ts | 9 +- packages/server/src/server/session.ts | 272 ++++++++++++------ .../src/services/agent-title-generator.ts | 47 +-- packages/server/tsconfig.server.json | 4 +- 11 files changed, 319 insertions(+), 229 deletions(-) diff --git a/packages/app/src/app/index.tsx b/packages/app/src/app/index.tsx index 30006baa6..3b4a009b2 100644 --- a/packages/app/src/app/index.tsx +++ b/packages/app/src/app/index.tsx @@ -1785,6 +1785,7 @@ const styles = StyleSheet.create((theme) => ({ minHeight: 0, }, scrollContent: { + paddingTop: theme.spacing[6], paddingBottom: theme.spacing[4], flexGrow: 1, }, diff --git a/packages/app/src/components/agent-stream-view.tsx b/packages/app/src/components/agent-stream-view.tsx index 389eb5ee0..c0dae2306 100644 --- a/packages/app/src/components/agent-stream-view.tsx +++ b/packages/app/src/components/agent-stream-view.tsx @@ -53,7 +53,7 @@ export function AgentStreamView({ {streamItems.length === 0 ? ( diff --git a/packages/app/src/hooks/use-websocket.ts b/packages/app/src/hooks/use-websocket.ts index 6baa10991..29a5157cb 100644 --- a/packages/app/src/hooks/use-websocket.ts +++ b/packages/app/src/hooks/use-websocket.ts @@ -48,7 +48,7 @@ export function useWebSocket(url: string, conversationId?: string | null): UseWe }; ws.onerror = (error) => { - console.error('[WS] Error:', error); + console.warn('[WS] Error:', error); }; ws.onmessage = (event) => { @@ -87,7 +87,7 @@ export function useWebSocket(url: string, conversationId?: string | null): UseWe wsRef.current = ws; } catch (err) { - console.error('[WS] Failed to create WebSocket:', err); + console.warn('[WS] Failed to create WebSocket:', err); } }, [url, conversationId]); diff --git a/packages/server/src/server/acp/activity-curator.ts b/packages/server/src/server/acp/activity-curator.ts index 194790883..70c87675b 100644 --- a/packages/server/src/server/acp/activity-curator.ts +++ b/packages/server/src/server/acp/activity-curator.ts @@ -9,28 +9,34 @@ export function curateAgentActivity(updates: AgentUpdate[]): string { let thoughtBuffer = ""; for (const update of updates) { - if (!update.notification.update) { + // Only process session notifications + if (update.notification.type !== 'session') { continue; } - const updateType = update.notification.update.sessionUpdate; - const content = (update.notification.update as any).content; + + const sessionUpdate = update.notification.notification.update; + const updateType = sessionUpdate.sessionUpdate; switch (updateType) { - case "agent_message_chunk": - if (content?.type === "text" && content?.text) { - messageBuffer += content.text; + case "agent_message_chunk": { + const chunk = sessionUpdate as Extract; + if (chunk.content?.type === "text" && chunk.content?.text) { + messageBuffer += chunk.content.text; } break; + } - case "agent_thought_chunk": - if (content?.type === "text" && content?.text) { - thoughtBuffer += content.text; + case "agent_thought_chunk": { + const chunk = sessionUpdate as Extract; + if (chunk.content?.type === "text" && chunk.content?.text) { + thoughtBuffer += chunk.content.text; } break; + } case "tool_call": case "tool_call_update": { - // Flush any buffered message before tool call + // Flush buffered content if (messageBuffer.trim()) { lines.push(messageBuffer.trim()); messageBuffer = ""; @@ -40,24 +46,23 @@ export function curateAgentActivity(updates: AgentUpdate[]): string { thoughtBuffer = ""; } - const data = update.notification.update as any; - const title = data.title || data.kind || "Tool"; - const status = data.status || "unknown"; + const toolUpdate = sessionUpdate as Extract; + const title = toolUpdate.title || toolUpdate.kind || "Tool"; + const status = toolUpdate.status || "unknown"; lines.push(`\n[${title}] ${status}`); - if (data.rawInput && Object.keys(data.rawInput).length > 0) { - lines.push(`Input: ${JSON.stringify(data.rawInput)}`); + if (toolUpdate.rawInput && Object.keys(toolUpdate.rawInput).length > 0) { + lines.push(`Input: ${JSON.stringify(toolUpdate.rawInput)}`); } - - if (data.rawOutput && Object.keys(data.rawOutput).length > 0) { - lines.push(`Output: ${JSON.stringify(data.rawOutput)}`); + if (toolUpdate.rawOutput && Object.keys(toolUpdate.rawOutput).length > 0) { + lines.push(`Output: ${JSON.stringify(toolUpdate.rawOutput)}`); } break; } case "plan": { - // Flush any buffered content before plan + // Flush buffered content if (messageBuffer.trim()) { lines.push(messageBuffer.trim()); messageBuffer = ""; @@ -67,23 +72,25 @@ export function curateAgentActivity(updates: AgentUpdate[]): string { thoughtBuffer = ""; } - const entries = (update.notification.update as any).entries; + const planUpdate = sessionUpdate as Extract; lines.push("\n[Plan]"); - for (const entry of entries) { + for (const entry of planUpdate.entries) { lines.push(`- [${entry.status}] ${entry.content}`); } break; } - case "user_message_chunk": - if (content?.type === "text" && content?.text) { - lines.push(`User: ${content.text}`); + case "user_message_chunk": { + const chunk = sessionUpdate as Extract; + if (chunk.content?.type === "text" && chunk.content?.text) { + lines.push(`User: ${chunk.content.text}`); } break; + } } } - // Flush any remaining buffered content + // Flush remaining buffered content if (messageBuffer.trim()) { lines.push(messageBuffer.trim()); } diff --git a/packages/server/src/server/acp/agent-manager.ts b/packages/server/src/server/acp/agent-manager.ts index 4bc37a36c..336010427 100644 --- a/packages/server/src/server/acp/agent-manager.ts +++ b/packages/server/src/server/acp/agent-manager.ts @@ -23,6 +23,7 @@ import type { AgentUpdateCallback, SessionMode, EnrichedSessionNotification, + EnrichedSessionUpdate, } from "./types.js"; interface PendingPermission { @@ -249,13 +250,13 @@ export class AgentManager { * Send a prompt to an agent * @param agentId - Agent ID * @param prompt - The prompt text - * @param options - Optional settings: maxWait (ms), sessionMode to set before sending + * @param options - Optional settings: maxWait (ms), sessionMode to set before sending, messageId for deduplication * @returns Object with didComplete boolean indicating if agent finished within maxWait time */ async sendPrompt( agentId: string, prompt: string, - options?: { maxWait?: number; sessionMode?: string } + options?: { maxWait?: number; sessionMode?: string; messageId?: string } ): Promise<{ didComplete: boolean; stopReason?: string }> { const agent = this.agents.get(agentId); if (!agent) { @@ -271,6 +272,38 @@ export class AgentManager { await this.setSessionMode(agentId, options.sessionMode); } + // Emit user message notification + const userMessageUpdate: AgentUpdate = { + agentId, + timestamp: new Date(), + notification: { + type: 'session', + notification: { + sessionId: agent.sessionId!, + update: { + sessionUpdate: 'user_message_chunk', + content: { + type: 'text', + text: prompt, + }, + ...(options?.messageId ? { messageId: options.messageId } : {}), + }, + }, + }, + }; + + // Store in history + agent.updates.push(userMessageUpdate); + + // Notify subscribers + for (const subscriber of agent.subscribers) { + try { + subscriber(userMessageUpdate); + } catch (error) { + console.error(`[Agent ${agentId}] Subscriber error:`, error); + } + } + agent.status = "processing"; this.notifySubscribers(agentId); @@ -416,7 +449,7 @@ export class AgentManager { return Array.from(this.agents.values()).map((agent) => ({ id: agent.id, status: agent.status, - createdAt: agent.createdAt.toISOString(), + createdAt: agent.createdAt, type: "claude" as const, sessionId: agent.sessionId ?? null, error: agent.error ?? null, @@ -527,7 +560,7 @@ export class AgentManager { if (!agent) return; // Augment update with stable message IDs for deduplication - let enrichedUpdate: EnrichedSessionNotification = update; + let enrichedUpdate: EnrichedSessionNotification; const updateType = update.update.sessionUpdate; @@ -541,7 +574,7 @@ export class AgentManager { update: { ...update.update, messageId: agent.currentAssistantMessageId, - }, + } as EnrichedSessionUpdate, }; } // Agent thought chunks - add stable message ID @@ -554,31 +587,33 @@ export class AgentManager { update: { ...update.update, messageId: agent.currentThoughtId, - }, + } as EnrichedSessionUpdate, }; } - // Reset message IDs on new turn (user message or tool call starts new turn) - else if (updateType === 'tool_call' || updateType === 'user_message_chunk') { - agent.currentAssistantMessageId = null; - agent.currentThoughtId = null; + // For other update types, use update as-is + else { + enrichedUpdate = update as EnrichedSessionNotification; + + // Reset message IDs on new turn (user message or tool call starts new turn) + if (updateType === 'tool_call' || updateType === 'user_message_chunk') { + agent.currentAssistantMessageId = null; + agent.currentThoughtId = null; + } } - // Create agent update with enriched notification + // Create agent update with enriched notification wrapped in discriminated union const agentUpdate: AgentUpdate = { agentId, timestamp: new Date(), - notification: enrichedUpdate, + notification: { + type: 'session', + notification: enrichedUpdate, + }, }; // Store the update in history agent.updates.push(agentUpdate); - // Handle mode change notifications - if ((update as any).type === "currentModeUpdate" && (update as any).currentModeId) { - agent.currentModeId = (update as any).currentModeId; - console.log(`[Agent ${agentId}] Mode changed to: ${agent.currentModeId}`); - } - // Log update for debugging console.log( `[Agent ${agentId}] Session update:`, @@ -606,13 +641,10 @@ export class AgentManager { agentId, timestamp: new Date(), notification: { - type: "sessionUpdate", - sessionUpdate: { - sessionId: agent.sessionId || "", - sessionUpdate: "status_change", - status: agent.status, - }, - } as any, + type: 'status', + status: agent.status, + error: agent.error, + }, }; for (const subscriber of agent.subscribers) { @@ -648,13 +680,10 @@ export class AgentManager { agentId, timestamp: new Date(), notification: { - type: "sessionUpdate", - sessionUpdate: { - sessionId: agent.sessionId || "", - status: agent.status, - state: agent.error ? { kind: "error", error: agent.error } : undefined, - }, - } as any, + type: 'status', + status: agent.status, + error: agent.error, + }, }; for (const subscriber of agent.subscribers) { @@ -743,23 +772,15 @@ export class AgentManager { agent.pendingPermissions.set(requestId, pendingPermission); - // Emit permission request via session notification + // Emit permission request via discriminated union // This will be picked up by subscribers (Session) and forwarded to UI - const permissionNotification: any = { - type: "permissionRequest", - permissionRequest: { - agentId, - requestId, - sessionId: params.sessionId, - toolCall: params.toolCall, - options: params.options, - }, - }; - const agentUpdate: AgentUpdate = { agentId, timestamp: new Date(), - notification: permissionNotification, + notification: { + type: 'permission', + request: params, + }, }; // Store the update in history diff --git a/packages/server/src/server/acp/mcp-server.ts b/packages/server/src/server/acp/mcp-server.ts index 893d0bd3e..2cf486b33 100644 --- a/packages/server/src/server/acp/mcp-server.ts +++ b/packages/server/src/server/acp/mcp-server.ts @@ -3,7 +3,7 @@ import { z } from "zod"; import { homedir } from "os"; import { resolve } from "path"; import { AgentManager } from "./agent-manager.js"; -import type { SessionNotification } from "@agentclientprotocol/sdk"; +import type { AgentNotification } from "./types.js"; import { curateAgentActivity } from "./activity-curator.js"; export interface AgentMcpServerOptions { @@ -21,10 +21,23 @@ function expandPath(path: string): string { } /** - * Extract the update type from a SessionNotification + * Extract the update type from an AgentNotification */ -function getUpdateType(notification: SessionNotification): string { - return notification.update.sessionUpdate; +function getUpdateType(notification: AgentNotification): string { + if (notification.type === 'session') { + return notification.notification.update.sessionUpdate; + } + return notification.type; +} + +/** + * Serialize AgentInfo for MCP output (convert Date to string for JSON compatibility) + */ +function serializeAgentInfo(info: any): any { + return { + ...info, + createdAt: info.createdAt.toISOString(), + }; } /** @@ -205,7 +218,7 @@ export async function createAgentMcpServer( const result = { status, - info: agentInfo, + info: serializeAgentInfo(agentInfo), }; return { @@ -246,7 +259,7 @@ export async function createAgentMcpServer( const agents = agentManager.listAgents(); const result = { - agents, + agents: agents.map(serializeAgentInfo), }; return { diff --git a/packages/server/src/server/acp/types.ts b/packages/server/src/server/acp/types.ts index bbc532d56..5a910275a 100644 --- a/packages/server/src/server/acp/types.ts +++ b/packages/server/src/server/acp/types.ts @@ -1,4 +1,4 @@ -import type { SessionNotification } from "@agentclientprotocol/sdk"; +import type { SessionNotification, RequestPermissionRequest } from "@agentclientprotocol/sdk"; /** * Extended update types with messageId for proper deduplication @@ -15,6 +15,14 @@ export interface EnrichedSessionNotification extends Omit> | null = null; + private terminalMcpClient: Awaited< + ReturnType + > | null = null; private terminalTools: Record | null = null; private terminalManager: any | null = null; - private agentMcpClient: Awaited> | null = null; + private agentMcpClient: Awaited< + ReturnType + > | null = null; private agentTools: Record | null = null; private agentManager: AgentManager; private agentUpdateUnsubscribers: Map void> = new Map(); @@ -137,7 +151,9 @@ export class Session { */ private subscribeToAgent(agentId: string): void { if (!this.agentManager) { - console.error(`[Session ${this.clientId}] Cannot subscribe to agent: AgentManager not initialized`); + console.error( + `[Session ${this.clientId}] Cannot subscribe to agent: AgentManager not initialized` + ); return; } @@ -149,48 +165,40 @@ export class Session { const unsubscribe = this.agentManager.subscribeToUpdates( agentId, (update: AgentUpdate) => { - // Check if this is a permission request - const notification = update.notification as any; - console.log(`[Session ${this.clientId}] Agent update notification type:`, notification.type); + const notification = update.notification; + console.log( + `[Session ${this.clientId}] Agent update notification type:`, + notification.type + ); - if (notification.type === "permissionRequest" && notification.permissionRequest) { - // Forward permission request as a dedicated message type - const permissionRequest = notification.permissionRequest; + // Handle permission requests + if (notification.type === "permission") { + const permissionRequest = notification.request; this.emit({ type: "agent_permission_request", payload: { - agentId: permissionRequest.agentId, - requestId: permissionRequest.requestId, + agentId, + requestId: uuidv4(), // Generate request ID sessionId: permissionRequest.sessionId, toolCall: permissionRequest.toolCall, options: permissionRequest.options, }, }); - console.log(`[Session ${this.clientId}] Forwarded permission request ${permissionRequest.requestId} for agent ${agentId}`); + console.log( + `[Session ${this.clientId}] Forwarded permission request for agent ${agentId}` + ); return; } - // Forward agent updates to WebSocket - this.emit({ - type: "agent_update", - payload: { - agentId: update.agentId, - timestamp: update.timestamp, - notification: update.notification, - }, - }); - - // Trigger title generation after first meaningful update - this.maybeTriggerTitleGeneration(agentId); - - // Check if this is a status change notification - // The agent manager sends custom notifications with status field - if (notification && notification.sessionUpdate && notification.sessionUpdate.status) { - const status = notification.sessionUpdate.status; + // Handle status updates + if (notification.type === "status") { + const status = notification.status; // Get current agent info try { - const info = this.agentManager!.listAgents().find(a => a.id === agentId); + const info = this.agentManager!.listAgents().find( + (a) => a.id === agentId + ); if (info) { // Emit agent_status message this.emit({ @@ -212,17 +220,41 @@ export class Session { }, }, }); - console.log(`[Session ${this.clientId}] Agent ${agentId} status changed to: ${status}`); + console.log( + `[Session ${this.clientId}] Agent ${agentId} status changed to: ${status}` + ); } } catch (error) { - console.error(`[Session ${this.clientId}] Failed to get agent info for status update:`, error); + console.error( + `[Session ${this.clientId}] Failed to get agent info for status update:`, + error + ); } + return; + } + + // Handle session notifications + if (notification.type === "session") { + // Forward agent updates to WebSocket + this.emit({ + type: "agent_update", + payload: { + agentId: update.agentId, + timestamp: update.timestamp, + notification: update.notification, + }, + }); + + // Trigger title generation after first meaningful update + this.maybeTriggerTitleGeneration(agentId); } } ); this.agentUpdateUnsubscribers.set(agentId, unsubscribe); - console.log(`[Session ${this.clientId}] Subscribed to agent ${agentId} updates`); + console.log( + `[Session ${this.clientId}] Subscribed to agent ${agentId} updates` + ); } /** @@ -243,8 +275,8 @@ export class Session { // Get agent updates const updates = this.agentManager.getAgentUpdates(agentId); - // Need at least a few updates before generating title - if (updates.length < 2) { + // Need at least 15 updates before generating title + if (updates.length < 15) { return; } @@ -252,12 +284,14 @@ export class Session { this.agentManager.markTitleGenerationTriggered(agentId); // Generate title in background - const info = this.agentManager.listAgents().find(a => a.id === agentId); + const info = this.agentManager.listAgents().find((a) => a.id === agentId); if (!info) { return; } - console.log(`[Session ${this.clientId}] Triggering title generation for agent ${agentId}`); + console.log( + `[Session ${this.clientId}] Triggering title generation for agent ${agentId}` + ); generateAgentTitle(updates, info.cwd) .then((title) => { @@ -265,7 +299,9 @@ export class Session { this.agentManager.setAgentTitle(agentId, title); // Emit agent_status to sync the new title to client - const updatedInfo = this.agentManager.listAgents().find(a => a.id === agentId); + const updatedInfo = this.agentManager + .listAgents() + .find((a) => a.id === agentId); if (updatedInfo) { this.emit({ type: "agent_status", @@ -289,7 +325,10 @@ export class Session { } }) .catch((error) => { - console.error(`[Session ${this.clientId}] Failed to generate title for agent ${agentId}:`, error); + console.error( + `[Session ${this.clientId}] Failed to generate title for agent ${agentId}:`, + error + ); }); } @@ -299,17 +338,20 @@ export class Session { private async initializeTerminalMcp(): Promise { try { // Create Terminal Manager directly - const { TerminalManager } = await import("./terminal-mcp/terminal-manager.js"); + const { TerminalManager } = await import( + "./terminal-mcp/terminal-manager.js" + ); this.terminalManager = new TerminalManager(this.conversationId); await this.terminalManager.initialize(); // Create Terminal MCP server with conversation-specific session const server = await createTerminalMcpServer({ - sessionName: this.conversationId + sessionName: this.conversationId, }); // Create linked transport pair - const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); // Connect server to its transport await server.connect(serverTransport); @@ -345,7 +387,8 @@ export class Session { }); // Create linked transport pair - const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); // Connect server to its transport await server.connect(serverTransport); @@ -359,7 +402,9 @@ export class Session { this.agentTools = await this.agentMcpClient.tools(); console.log( - `[Session ${this.clientId}] Agent MCP initialized with ${Object.keys(this.agentTools).length} tools` + `[Session ${this.clientId}] Agent MCP initialized with ${ + Object.keys(this.agentTools).length + } tools` ); } catch (error) { console.error( @@ -409,7 +454,11 @@ export class Session { break; case "send_agent_message": - await this.handleSendAgentMessage(msg.agentId, msg.text, msg.messageId); + await this.handleSendAgentMessage( + msg.agentId, + msg.text, + msg.messageId + ); break; case "send_agent_audio": @@ -425,7 +474,11 @@ export class Session { break; case "agent_permission_response": - await this.handleAgentPermissionResponse(msg.agentId, msg.requestId, msg.optionId); + await this.handleAgentPermissionResponse( + msg.agentId, + msg.requestId, + msg.optionId + ); break; } } catch (error: any) { @@ -471,7 +524,7 @@ export class Session { this.emit({ type: "list_conversations_response", payload: { - conversations: conversations.map(conv => ({ + conversations: conversations.map((conv) => ({ id: conv.id, lastUpdated: conv.lastUpdated.toISOString(), messageCount: conv.messageCount, @@ -533,41 +586,29 @@ export class Session { private handleSetRealtimeMode(enabled: boolean): void { this.isRealtimeMode = enabled; console.log( - `[Session ${this.clientId}] Realtime mode ${enabled ? "enabled" : "disabled"}` + `[Session ${this.clientId}] Realtime mode ${ + enabled ? "enabled" : "disabled" + }` ); } /** * Handle text message to agent */ - private async handleSendAgentMessage(agentId: string, text: string, messageId?: string): Promise { + private async handleSendAgentMessage( + agentId: string, + text: string, + messageId?: string + ): Promise { console.log( - `[Session ${this.clientId}] Sending text to agent ${agentId}: ${text.substring(0, 50)}...` + `[Session ${ + this.clientId + }] Sending text to agent ${agentId}: ${text.substring(0, 50)}...` ); try { - // Emit user message notification before sending to agent - // (Claude Code ACP doesn't echo user messages, so we do it manually) - this.emit({ - type: "agent_update", - payload: { - agentId, - timestamp: new Date(), - notification: { - type: "sessionUpdate", - update: { - sessionUpdate: "user_message_chunk", - content: { - type: "text", - text: text, - }, - ...(messageId ? { messageId } : {}), - }, - } as any, - }, - }); - - await this.agentManager.sendPrompt(agentId, text); + // sendPrompt will emit the user message notification + await this.agentManager.sendPrompt(agentId, text, { messageId }); console.log(`[Session ${this.clientId}] Sent text to agent ${agentId}`); } catch (error: any) { console.error( @@ -630,7 +671,9 @@ export class Session { // Send transcribed text to agent await this.agentManager.sendPrompt(agentId, transcriptText); - console.log(`[Session ${this.clientId}] Sent transcribed text to agent ${agentId}`); + console.log( + `[Session ${this.clientId}] Sent transcribed text to agent ${agentId}` + ); } catch (error: any) { console.error( `[Session ${this.clientId}] Failed to process audio for agent ${agentId}:`, @@ -652,9 +695,14 @@ export class Session { /** * Handle create agent request */ - private async handleCreateAgentRequest(cwd: string, initialMode?: string): Promise { + private async handleCreateAgentRequest( + cwd: string, + initialMode?: string + ): Promise { console.log( - `[Session ${this.clientId}] Creating agent in ${cwd} with mode ${initialMode || "default"}` + `[Session ${this.clientId}] Creating agent in ${cwd} with mode ${ + initialMode || "default" + }` ); try { @@ -666,10 +714,12 @@ export class Session { console.log(`[Session ${this.clientId}] Created agent ${agentId}`); // Get agent info - const agentInfo = this.agentManager.listAgents().find(a => a.id === agentId); + const agentInfo = this.agentManager + .listAgents() + .find((a) => a.id === agentId); console.log(`[Session ${this.clientId}] Agent info:`, { currentModeId: agentInfo?.currentModeId, - availableModes: agentInfo?.availableModes + availableModes: agentInfo?.availableModes, }); // Subscribe to agent updates @@ -688,7 +738,10 @@ export class Session { cwd: agentInfo?.cwd || cwd, }, }); - console.log(`[Session ${this.clientId}] Emitted agent_created with currentModeId:`, agentInfo?.currentModeId); + console.log( + `[Session ${this.clientId}] Emitted agent_created with currentModeId:`, + agentInfo?.currentModeId + ); } catch (error: any) { console.error( `[Session ${this.clientId}] Failed to create agent:`, @@ -710,15 +763,22 @@ export class Session { /** * Handle set agent mode request */ - private async handleSetAgentMode(agentId: string, modeId: string): Promise { - console.log(`[Session ${this.clientId}] Setting agent ${agentId} mode to ${modeId}`); + private async handleSetAgentMode( + agentId: string, + modeId: string + ): Promise { + console.log( + `[Session ${this.clientId}] Setting agent ${agentId} mode to ${modeId}` + ); try { await this.agentManager.setSessionMode(agentId, modeId); - console.log(`[Session ${this.clientId}] Agent ${agentId} mode set to ${modeId}`); + console.log( + `[Session ${this.clientId}] Agent ${agentId} mode set to ${modeId}` + ); // Emit agent_status to notify client of mode change - const info = this.agentManager.listAgents().find(a => a.id === agentId); + const info = this.agentManager.listAgents().find((a) => a.id === agentId); if (info) { this.emit({ type: "agent_status", @@ -741,7 +801,10 @@ export class Session { }); } } catch (error: any) { - console.error(`[Session ${this.clientId}] Failed to set agent mode:`, error); + console.error( + `[Session ${this.clientId}] Failed to set agent mode:`, + error + ); this.emit({ type: "activity_log", payload: { @@ -758,14 +821,25 @@ export class Session { /** * Handle agent permission response from user */ - private async handleAgentPermissionResponse(agentId: string, requestId: string, optionId: string): Promise { - console.log(`[Session ${this.clientId}] Handling permission response for agent ${agentId}, request ${requestId}, option ${optionId}`); + private async handleAgentPermissionResponse( + agentId: string, + requestId: string, + optionId: string + ): Promise { + console.log( + `[Session ${this.clientId}] Handling permission response for agent ${agentId}, request ${requestId}, option ${optionId}` + ); try { this.agentManager.respondToPermission(agentId, requestId, optionId); - console.log(`[Session ${this.clientId}] Permission response forwarded to agent ${agentId}`); + console.log( + `[Session ${this.clientId}] Permission response forwarded to agent ${agentId}` + ); } catch (error: any) { - console.error(`[Session ${this.clientId}] Failed to respond to permission:`, error); + console.error( + `[Session ${this.clientId}] Failed to respond to permission:`, + error + ); this.emit({ type: "activity_log", payload: { @@ -803,7 +877,10 @@ export class Session { type: "agent_update", payload: { agentId: update.agentId, - timestamp: update.timestamp instanceof Date ? update.timestamp : new Date(update.timestamp), + timestamp: + update.timestamp instanceof Date + ? update.timestamp + : new Date(update.timestamp), notification: update.notification, }, }); @@ -1123,6 +1200,11 @@ export class Session { const result = await streamText({ model: openrouter("anthropic/claude-haiku-4.5"), system: getSystemPrompt(), + providerOptions: { + openrouter: { + transforms: ["middle-out"], // Compress prompts that are > context size. + } as OpenRouterProviderOptions, + }, messages: this.messages, tools: allTools, abortSignal: this.abortController.signal, @@ -1203,7 +1285,9 @@ export class Session { this.subscribeToAgent(agentId); // Get full agent info and emit agent_created message - const agentInfo = this.agentManager.listAgents().find(a => a.id === agentId); + const agentInfo = this.agentManager + .listAgents() + .find((a) => a.id === agentId); if (agentInfo) { this.emit({ type: "agent_created", @@ -1558,7 +1642,9 @@ export class Session { for (const [agentId, unsubscribe] of this.agentUpdateUnsubscribers) { try { unsubscribe(); - console.log(`[Session ${this.clientId}] Unsubscribed from agent ${agentId}`); + console.log( + `[Session ${this.clientId}] Unsubscribed from agent ${agentId}` + ); } catch (error) { console.error( `[Session ${this.clientId}] Failed to unsubscribe from agent ${agentId}:`, diff --git a/packages/server/src/services/agent-title-generator.ts b/packages/server/src/services/agent-title-generator.ts index 4c0a264a0..a00abcd71 100644 --- a/packages/server/src/services/agent-title-generator.ts +++ b/packages/server/src/services/agent-title-generator.ts @@ -2,6 +2,7 @@ import { generateObject } from "ai"; import { createOpenAI } from "@ai-sdk/openai"; import { z } from "zod"; import type { AgentUpdate } from "../server/acp/types.js"; +import { curateAgentActivity } from "../server/acp/activity-curator.js"; let openai: ReturnType | null = null; @@ -14,48 +15,6 @@ export function isTitleGeneratorInitialized(): boolean { return openai !== null; } -/** - * Extract text context from agent updates for title generation - */ -function extractActivityContext(updates: AgentUpdate[]): string { - const lines: string[] = []; - - for (const update of updates.slice(0, 10)) { // Only use first 10 updates for context - const notification = update.notification as any; - - if (notification?.update?.sessionUpdate) { - const sessionUpdate = notification.update.sessionUpdate; - - // Extract user messages - if (sessionUpdate.userMessage?.text) { - lines.push(`User: ${sessionUpdate.userMessage.text}`); - } - - // Extract assistant messages - if (sessionUpdate.assistantMessage?.text) { - lines.push(`Assistant: ${sessionUpdate.assistantMessage.text}`); - } - - // Extract tool calls (showing what actions were taken) - if (sessionUpdate.toolCall) { - const toolCall = sessionUpdate.toolCall; - lines.push(`Tool: ${toolCall.toolName || 'unknown'}`); - } - - // Extract plan entries - if (sessionUpdate.plan?.entries) { - for (const entry of sessionUpdate.plan.entries.slice(0, 3)) { - if (entry.content) { - lines.push(`Plan: ${entry.content}`); - } - } - } - } - } - - return lines.join('\n'); -} - /** * Generate a concise title for an agent based on its activity * Returns a 3-5 word title similar to ChatGPT/Claude.ai @@ -72,9 +31,9 @@ export async function generateAgentTitle( return "New Agent"; } - const activityContext = extractActivityContext(agentUpdates); + const activityContext = curateAgentActivity(agentUpdates); - if (!activityContext.trim()) { + if (!activityContext.trim() || activityContext === "No activity to display.") { return "New Agent"; } diff --git a/packages/server/tsconfig.server.json b/packages/server/tsconfig.server.json index b309d6753..f0cadd7bc 100644 --- a/packages/server/tsconfig.server.json +++ b/packages/server/tsconfig.server.json @@ -14,11 +14,11 @@ "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, "outDir": "./dist/server", - "rootDir": "./src/server", + "rootDir": "./src", "declaration": true, "declarationMap": true, "sourceMap": true }, - "include": ["src/server/**/*"], + "include": ["src/server/**/*", "src/services/**/*"], "exclude": ["node_modules", "dist"] }