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
```
This commit is contained in:
Mohamed Boudra
2025-10-23 09:01:38 +02:00
parent c6cdc09a0b
commit 0893a9dc33
11 changed files with 319 additions and 229 deletions
+1
View File
@@ -1785,6 +1785,7 @@ const styles = StyleSheet.create((theme) => ({
minHeight: 0, minHeight: 0,
}, },
scrollContent: { scrollContent: {
paddingTop: theme.spacing[6],
paddingBottom: theme.spacing[4], paddingBottom: theme.spacing[4],
flexGrow: 1, flexGrow: 1,
}, },
@@ -53,7 +53,7 @@ export function AgentStreamView({
<ScrollView <ScrollView
ref={scrollViewRef} ref={scrollViewRef}
style={stylesheet.scrollView} style={stylesheet.scrollView}
contentContainerStyle={{ paddingTop: 16, paddingBottom: Math.max(insets.bottom, 32) }} contentContainerStyle={{ paddingTop: 24, paddingBottom: Math.max(insets.bottom, 32) }}
> >
{streamItems.length === 0 ? ( {streamItems.length === 0 ? (
<View style={stylesheet.emptyState}> <View style={stylesheet.emptyState}>
+2 -2
View File
@@ -48,7 +48,7 @@ export function useWebSocket(url: string, conversationId?: string | null): UseWe
}; };
ws.onerror = (error) => { ws.onerror = (error) => {
console.error('[WS] Error:', error); console.warn('[WS] Error:', error);
}; };
ws.onmessage = (event) => { ws.onmessage = (event) => {
@@ -87,7 +87,7 @@ export function useWebSocket(url: string, conversationId?: string | null): UseWe
wsRef.current = ws; wsRef.current = ws;
} catch (err) { } catch (err) {
console.error('[WS] Failed to create WebSocket:', err); console.warn('[WS] Failed to create WebSocket:', err);
} }
}, [url, conversationId]); }, [url, conversationId]);
@@ -9,28 +9,34 @@ export function curateAgentActivity(updates: AgentUpdate[]): string {
let thoughtBuffer = ""; let thoughtBuffer = "";
for (const update of updates) { for (const update of updates) {
if (!update.notification.update) { // Only process session notifications
if (update.notification.type !== 'session') {
continue; 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) { switch (updateType) {
case "agent_message_chunk": case "agent_message_chunk": {
if (content?.type === "text" && content?.text) { const chunk = sessionUpdate as Extract<typeof sessionUpdate, { sessionUpdate: 'agent_message_chunk' }>;
messageBuffer += content.text; if (chunk.content?.type === "text" && chunk.content?.text) {
messageBuffer += chunk.content.text;
} }
break; break;
}
case "agent_thought_chunk": case "agent_thought_chunk": {
if (content?.type === "text" && content?.text) { const chunk = sessionUpdate as Extract<typeof sessionUpdate, { sessionUpdate: 'agent_thought_chunk' }>;
thoughtBuffer += content.text; if (chunk.content?.type === "text" && chunk.content?.text) {
thoughtBuffer += chunk.content.text;
} }
break; break;
}
case "tool_call": case "tool_call":
case "tool_call_update": { case "tool_call_update": {
// Flush any buffered message before tool call // Flush buffered content
if (messageBuffer.trim()) { if (messageBuffer.trim()) {
lines.push(messageBuffer.trim()); lines.push(messageBuffer.trim());
messageBuffer = ""; messageBuffer = "";
@@ -40,24 +46,23 @@ export function curateAgentActivity(updates: AgentUpdate[]): string {
thoughtBuffer = ""; thoughtBuffer = "";
} }
const data = update.notification.update as any; const toolUpdate = sessionUpdate as Extract<typeof sessionUpdate, { sessionUpdate: 'tool_call' | 'tool_call_update' }>;
const title = data.title || data.kind || "Tool"; const title = toolUpdate.title || toolUpdate.kind || "Tool";
const status = data.status || "unknown"; const status = toolUpdate.status || "unknown";
lines.push(`\n[${title}] ${status}`); lines.push(`\n[${title}] ${status}`);
if (data.rawInput && Object.keys(data.rawInput).length > 0) { if (toolUpdate.rawInput && Object.keys(toolUpdate.rawInput).length > 0) {
lines.push(`Input: ${JSON.stringify(data.rawInput)}`); lines.push(`Input: ${JSON.stringify(toolUpdate.rawInput)}`);
} }
if (toolUpdate.rawOutput && Object.keys(toolUpdate.rawOutput).length > 0) {
if (data.rawOutput && Object.keys(data.rawOutput).length > 0) { lines.push(`Output: ${JSON.stringify(toolUpdate.rawOutput)}`);
lines.push(`Output: ${JSON.stringify(data.rawOutput)}`);
} }
break; break;
} }
case "plan": { case "plan": {
// Flush any buffered content before plan // Flush buffered content
if (messageBuffer.trim()) { if (messageBuffer.trim()) {
lines.push(messageBuffer.trim()); lines.push(messageBuffer.trim());
messageBuffer = ""; messageBuffer = "";
@@ -67,23 +72,25 @@ export function curateAgentActivity(updates: AgentUpdate[]): string {
thoughtBuffer = ""; thoughtBuffer = "";
} }
const entries = (update.notification.update as any).entries; const planUpdate = sessionUpdate as Extract<typeof sessionUpdate, { sessionUpdate: 'plan' }>;
lines.push("\n[Plan]"); lines.push("\n[Plan]");
for (const entry of entries) { for (const entry of planUpdate.entries) {
lines.push(`- [${entry.status}] ${entry.content}`); lines.push(`- [${entry.status}] ${entry.content}`);
} }
break; break;
} }
case "user_message_chunk": case "user_message_chunk": {
if (content?.type === "text" && content?.text) { const chunk = sessionUpdate as Extract<typeof sessionUpdate, { sessionUpdate: 'user_message_chunk' }>;
lines.push(`User: ${content.text}`); if (chunk.content?.type === "text" && chunk.content?.text) {
lines.push(`User: ${chunk.content.text}`);
} }
break; break;
}
} }
} }
// Flush any remaining buffered content // Flush remaining buffered content
if (messageBuffer.trim()) { if (messageBuffer.trim()) {
lines.push(messageBuffer.trim()); lines.push(messageBuffer.trim());
} }
+66 -45
View File
@@ -23,6 +23,7 @@ import type {
AgentUpdateCallback, AgentUpdateCallback,
SessionMode, SessionMode,
EnrichedSessionNotification, EnrichedSessionNotification,
EnrichedSessionUpdate,
} from "./types.js"; } from "./types.js";
interface PendingPermission { interface PendingPermission {
@@ -249,13 +250,13 @@ export class AgentManager {
* Send a prompt to an agent * Send a prompt to an agent
* @param agentId - Agent ID * @param agentId - Agent ID
* @param prompt - The prompt text * @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 * @returns Object with didComplete boolean indicating if agent finished within maxWait time
*/ */
async sendPrompt( async sendPrompt(
agentId: string, agentId: string,
prompt: string, prompt: string,
options?: { maxWait?: number; sessionMode?: string } options?: { maxWait?: number; sessionMode?: string; messageId?: string }
): Promise<{ didComplete: boolean; stopReason?: string }> { ): Promise<{ didComplete: boolean; stopReason?: string }> {
const agent = this.agents.get(agentId); const agent = this.agents.get(agentId);
if (!agent) { if (!agent) {
@@ -271,6 +272,38 @@ export class AgentManager {
await this.setSessionMode(agentId, options.sessionMode); 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"; agent.status = "processing";
this.notifySubscribers(agentId); this.notifySubscribers(agentId);
@@ -416,7 +449,7 @@ export class AgentManager {
return Array.from(this.agents.values()).map((agent) => ({ return Array.from(this.agents.values()).map((agent) => ({
id: agent.id, id: agent.id,
status: agent.status, status: agent.status,
createdAt: agent.createdAt.toISOString(), createdAt: agent.createdAt,
type: "claude" as const, type: "claude" as const,
sessionId: agent.sessionId ?? null, sessionId: agent.sessionId ?? null,
error: agent.error ?? null, error: agent.error ?? null,
@@ -527,7 +560,7 @@ export class AgentManager {
if (!agent) return; if (!agent) return;
// Augment update with stable message IDs for deduplication // Augment update with stable message IDs for deduplication
let enrichedUpdate: EnrichedSessionNotification = update; let enrichedUpdate: EnrichedSessionNotification;
const updateType = update.update.sessionUpdate; const updateType = update.update.sessionUpdate;
@@ -541,7 +574,7 @@ export class AgentManager {
update: { update: {
...update.update, ...update.update,
messageId: agent.currentAssistantMessageId, messageId: agent.currentAssistantMessageId,
}, } as EnrichedSessionUpdate,
}; };
} }
// Agent thought chunks - add stable message ID // Agent thought chunks - add stable message ID
@@ -554,31 +587,33 @@ export class AgentManager {
update: { update: {
...update.update, ...update.update,
messageId: agent.currentThoughtId, messageId: agent.currentThoughtId,
}, } as EnrichedSessionUpdate,
}; };
} }
// Reset message IDs on new turn (user message or tool call starts new turn) // For other update types, use update as-is
else if (updateType === 'tool_call' || updateType === 'user_message_chunk') { else {
agent.currentAssistantMessageId = null; enrichedUpdate = update as EnrichedSessionNotification;
agent.currentThoughtId = null;
// 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 = { const agentUpdate: AgentUpdate = {
agentId, agentId,
timestamp: new Date(), timestamp: new Date(),
notification: enrichedUpdate, notification: {
type: 'session',
notification: enrichedUpdate,
},
}; };
// Store the update in history // Store the update in history
agent.updates.push(agentUpdate); 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 // Log update for debugging
console.log( console.log(
`[Agent ${agentId}] Session update:`, `[Agent ${agentId}] Session update:`,
@@ -606,13 +641,10 @@ export class AgentManager {
agentId, agentId,
timestamp: new Date(), timestamp: new Date(),
notification: { notification: {
type: "sessionUpdate", type: 'status',
sessionUpdate: { status: agent.status,
sessionId: agent.sessionId || "", error: agent.error,
sessionUpdate: "status_change", },
status: agent.status,
},
} as any,
}; };
for (const subscriber of agent.subscribers) { for (const subscriber of agent.subscribers) {
@@ -648,13 +680,10 @@ export class AgentManager {
agentId, agentId,
timestamp: new Date(), timestamp: new Date(),
notification: { notification: {
type: "sessionUpdate", type: 'status',
sessionUpdate: { status: agent.status,
sessionId: agent.sessionId || "", error: agent.error,
status: agent.status, },
state: agent.error ? { kind: "error", error: agent.error } : undefined,
},
} as any,
}; };
for (const subscriber of agent.subscribers) { for (const subscriber of agent.subscribers) {
@@ -743,23 +772,15 @@ export class AgentManager {
agent.pendingPermissions.set(requestId, pendingPermission); 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 // 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 = { const agentUpdate: AgentUpdate = {
agentId, agentId,
timestamp: new Date(), timestamp: new Date(),
notification: permissionNotification, notification: {
type: 'permission',
request: params,
},
}; };
// Store the update in history // Store the update in history
+19 -6
View File
@@ -3,7 +3,7 @@ import { z } from "zod";
import { homedir } from "os"; import { homedir } from "os";
import { resolve } from "path"; import { resolve } from "path";
import { AgentManager } from "./agent-manager.js"; import { AgentManager } from "./agent-manager.js";
import type { SessionNotification } from "@agentclientprotocol/sdk"; import type { AgentNotification } from "./types.js";
import { curateAgentActivity } from "./activity-curator.js"; import { curateAgentActivity } from "./activity-curator.js";
export interface AgentMcpServerOptions { 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 { function getUpdateType(notification: AgentNotification): string {
return notification.update.sessionUpdate; 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 = { const result = {
status, status,
info: agentInfo, info: serializeAgentInfo(agentInfo),
}; };
return { return {
@@ -246,7 +259,7 @@ export async function createAgentMcpServer(
const agents = agentManager.listAgents(); const agents = agentManager.listAgents();
const result = { const result = {
agents, agents: agents.map(serializeAgentInfo),
}; };
return { return {
+12 -4
View File
@@ -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 * Extended update types with messageId for proper deduplication
@@ -15,6 +15,14 @@ export interface EnrichedSessionNotification extends Omit<SessionNotification, '
update: EnrichedSessionUpdate; update: EnrichedSessionUpdate;
} }
/**
* Discriminated union for all notification types in the agent update stream
*/
export type AgentNotification =
| { type: 'session'; notification: EnrichedSessionNotification }
| { type: 'permission'; request: RequestPermissionRequest }
| { type: 'status'; status: AgentStatus; error?: string };
/** /**
* Status of an agent * Status of an agent
*/ */
@@ -32,7 +40,7 @@ export type AgentStatus =
export interface AgentInfo { export interface AgentInfo {
id: string; id: string;
status: AgentStatus; status: AgentStatus;
createdAt: string; createdAt: Date;
type: "claude"; type: "claude";
sessionId: string | null; sessionId: string | null;
error: string | null; error: string | null;
@@ -44,12 +52,12 @@ export interface AgentInfo {
/** /**
* Update from an agent session * Update from an agent session
* Wraps enriched SessionNotification with additional metadata * Wraps all notification types with additional metadata
*/ */
export interface AgentUpdate { export interface AgentUpdate {
agentId: string; agentId: string;
timestamp: Date; timestamp: Date;
notification: EnrichedSessionNotification; notification: AgentNotification;
} }
/** /**
+2 -7
View File
@@ -286,14 +286,9 @@ export const AgentPermissionRequestMessageSchema = z.object({
agentId: z.string(), agentId: z.string(),
requestId: z.string(), requestId: z.string(),
sessionId: z.string(), sessionId: z.string(),
toolCall: z.object({ toolCall: z.any(), // ToolCallUpdate from ACP SDK - complex type with flexible rawInput
toolCallId: z.string(),
rawInput: z.object({
plan: z.string(),
}),
}),
options: z.array(z.object({ options: z.array(z.object({
kind: z.enum(["allow_always", "allow_once", "reject_once"]), kind: z.enum(["allow_always", "allow_once", "reject_once", "reject_always"]),
name: z.string(), name: z.string(),
optionId: z.string(), optionId: z.string(),
})), })),
+179 -93
View File
@@ -6,7 +6,10 @@ import { join } from "path";
import invariant from "tiny-invariant"; import invariant from "tiny-invariant";
import { streamText, stepCountIs } from "ai"; import { streamText, stepCountIs } from "ai";
import type { ModelMessage } from "@ai-sdk/provider-utils"; import type { ModelMessage } from "@ai-sdk/provider-utils";
import { createOpenRouter } from "@openrouter/ai-sdk-provider"; import {
createOpenRouter,
OpenRouterProviderOptions,
} from "@openrouter/ai-sdk-provider";
import type { import type {
SessionInboundMessage, SessionInboundMessage,
SessionOutboundMessage, SessionOutboundMessage,
@@ -15,14 +18,21 @@ import { getSystemPrompt } from "./agent/system-prompt.js";
import { getAllTools } from "./agent/llm-openai.js"; import { getAllTools } from "./agent/llm-openai.js";
import { TTSManager } from "./agent/tts-manager.js"; import { TTSManager } from "./agent/tts-manager.js";
import { STTManager } from "./agent/stt-manager.js"; import { STTManager } from "./agent/stt-manager.js";
import { saveConversation, listConversations, deleteConversation } from "./persistence.js"; import {
saveConversation,
listConversations,
deleteConversation,
} from "./persistence.js";
import { experimental_createMCPClient } from "ai"; import { experimental_createMCPClient } from "ai";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { createTerminalMcpServer } from "./terminal-mcp/index.js"; import { createTerminalMcpServer } from "./terminal-mcp/index.js";
import { AgentManager } from "./acp/agent-manager.js"; import { AgentManager } from "./acp/agent-manager.js";
import { createAgentMcpServer } from "./acp/mcp-server.js"; import { createAgentMcpServer } from "./acp/mcp-server.js";
import type { AgentUpdate } from "./acp/types.js"; import type { AgentUpdate } from "./acp/types.js";
import { generateAgentTitle, isTitleGeneratorInitialized } from "../services/agent-title-generator.js"; import {
generateAgentTitle,
isTitleGeneratorInitialized,
} from "../services/agent-title-generator.js";
const execAsync = promisify(exec); const execAsync = promisify(exec);
@@ -72,10 +82,14 @@ export class Session {
private readonly sttManager: STTManager; private readonly sttManager: STTManager;
// Per-session MCP client and tools // Per-session MCP client and tools
private terminalMcpClient: Awaited<ReturnType<typeof experimental_createMCPClient>> | null = null; private terminalMcpClient: Awaited<
ReturnType<typeof experimental_createMCPClient>
> | null = null;
private terminalTools: Record<string, any> | null = null; private terminalTools: Record<string, any> | null = null;
private terminalManager: any | null = null; private terminalManager: any | null = null;
private agentMcpClient: Awaited<ReturnType<typeof experimental_createMCPClient>> | null = null; private agentMcpClient: Awaited<
ReturnType<typeof experimental_createMCPClient>
> | null = null;
private agentTools: Record<string, any> | null = null; private agentTools: Record<string, any> | null = null;
private agentManager: AgentManager; private agentManager: AgentManager;
private agentUpdateUnsubscribers: Map<string, () => void> = new Map(); private agentUpdateUnsubscribers: Map<string, () => void> = new Map();
@@ -137,7 +151,9 @@ export class Session {
*/ */
private subscribeToAgent(agentId: string): void { private subscribeToAgent(agentId: string): void {
if (!this.agentManager) { 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; return;
} }
@@ -149,48 +165,40 @@ export class Session {
const unsubscribe = this.agentManager.subscribeToUpdates( const unsubscribe = this.agentManager.subscribeToUpdates(
agentId, agentId,
(update: AgentUpdate) => { (update: AgentUpdate) => {
// Check if this is a permission request const notification = update.notification;
const notification = update.notification as any; console.log(
console.log(`[Session ${this.clientId}] Agent update notification type:`, notification.type); `[Session ${this.clientId}] Agent update notification type:`,
notification.type
);
if (notification.type === "permissionRequest" && notification.permissionRequest) { // Handle permission requests
// Forward permission request as a dedicated message type if (notification.type === "permission") {
const permissionRequest = notification.permissionRequest; const permissionRequest = notification.request;
this.emit({ this.emit({
type: "agent_permission_request", type: "agent_permission_request",
payload: { payload: {
agentId: permissionRequest.agentId, agentId,
requestId: permissionRequest.requestId, requestId: uuidv4(), // Generate request ID
sessionId: permissionRequest.sessionId, sessionId: permissionRequest.sessionId,
toolCall: permissionRequest.toolCall, toolCall: permissionRequest.toolCall,
options: permissionRequest.options, 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; return;
} }
// Forward agent updates to WebSocket // Handle status updates
this.emit({ if (notification.type === "status") {
type: "agent_update", const status = notification.status;
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;
// Get current agent info // Get current agent info
try { try {
const info = this.agentManager!.listAgents().find(a => a.id === agentId); const info = this.agentManager!.listAgents().find(
(a) => a.id === agentId
);
if (info) { if (info) {
// Emit agent_status message // Emit agent_status message
this.emit({ 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) { } 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); 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 // Get agent updates
const updates = this.agentManager.getAgentUpdates(agentId); const updates = this.agentManager.getAgentUpdates(agentId);
// Need at least a few updates before generating title // Need at least 15 updates before generating title
if (updates.length < 2) { if (updates.length < 15) {
return; return;
} }
@@ -252,12 +284,14 @@ export class Session {
this.agentManager.markTitleGenerationTriggered(agentId); this.agentManager.markTitleGenerationTriggered(agentId);
// Generate title in background // 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) { if (!info) {
return; 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) generateAgentTitle(updates, info.cwd)
.then((title) => { .then((title) => {
@@ -265,7 +299,9 @@ export class Session {
this.agentManager.setAgentTitle(agentId, title); this.agentManager.setAgentTitle(agentId, title);
// Emit agent_status to sync the new title to client // 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) { if (updatedInfo) {
this.emit({ this.emit({
type: "agent_status", type: "agent_status",
@@ -289,7 +325,10 @@ export class Session {
} }
}) })
.catch((error) => { .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<void> { private async initializeTerminalMcp(): Promise<void> {
try { try {
// Create Terminal Manager directly // 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); this.terminalManager = new TerminalManager(this.conversationId);
await this.terminalManager.initialize(); await this.terminalManager.initialize();
// Create Terminal MCP server with conversation-specific session // Create Terminal MCP server with conversation-specific session
const server = await createTerminalMcpServer({ const server = await createTerminalMcpServer({
sessionName: this.conversationId sessionName: this.conversationId,
}); });
// Create linked transport pair // Create linked transport pair
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
// Connect server to its transport // Connect server to its transport
await server.connect(serverTransport); await server.connect(serverTransport);
@@ -345,7 +387,8 @@ export class Session {
}); });
// Create linked transport pair // Create linked transport pair
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
// Connect server to its transport // Connect server to its transport
await server.connect(serverTransport); await server.connect(serverTransport);
@@ -359,7 +402,9 @@ export class Session {
this.agentTools = await this.agentMcpClient.tools(); this.agentTools = await this.agentMcpClient.tools();
console.log( 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) { } catch (error) {
console.error( console.error(
@@ -409,7 +454,11 @@ export class Session {
break; break;
case "send_agent_message": case "send_agent_message":
await this.handleSendAgentMessage(msg.agentId, msg.text, msg.messageId); await this.handleSendAgentMessage(
msg.agentId,
msg.text,
msg.messageId
);
break; break;
case "send_agent_audio": case "send_agent_audio":
@@ -425,7 +474,11 @@ export class Session {
break; break;
case "agent_permission_response": case "agent_permission_response":
await this.handleAgentPermissionResponse(msg.agentId, msg.requestId, msg.optionId); await this.handleAgentPermissionResponse(
msg.agentId,
msg.requestId,
msg.optionId
);
break; break;
} }
} catch (error: any) { } catch (error: any) {
@@ -471,7 +524,7 @@ export class Session {
this.emit({ this.emit({
type: "list_conversations_response", type: "list_conversations_response",
payload: { payload: {
conversations: conversations.map(conv => ({ conversations: conversations.map((conv) => ({
id: conv.id, id: conv.id,
lastUpdated: conv.lastUpdated.toISOString(), lastUpdated: conv.lastUpdated.toISOString(),
messageCount: conv.messageCount, messageCount: conv.messageCount,
@@ -533,41 +586,29 @@ export class Session {
private handleSetRealtimeMode(enabled: boolean): void { private handleSetRealtimeMode(enabled: boolean): void {
this.isRealtimeMode = enabled; this.isRealtimeMode = enabled;
console.log( console.log(
`[Session ${this.clientId}] Realtime mode ${enabled ? "enabled" : "disabled"}` `[Session ${this.clientId}] Realtime mode ${
enabled ? "enabled" : "disabled"
}`
); );
} }
/** /**
* Handle text message to agent * Handle text message to agent
*/ */
private async handleSendAgentMessage(agentId: string, text: string, messageId?: string): Promise<void> { private async handleSendAgentMessage(
agentId: string,
text: string,
messageId?: string
): Promise<void> {
console.log( 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 { try {
// Emit user message notification before sending to agent // sendPrompt will emit the user message notification
// (Claude Code ACP doesn't echo user messages, so we do it manually) await this.agentManager.sendPrompt(agentId, text, { messageId });
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);
console.log(`[Session ${this.clientId}] Sent text to agent ${agentId}`); console.log(`[Session ${this.clientId}] Sent text to agent ${agentId}`);
} catch (error: any) { } catch (error: any) {
console.error( console.error(
@@ -630,7 +671,9 @@ export class Session {
// Send transcribed text to agent // Send transcribed text to agent
await this.agentManager.sendPrompt(agentId, transcriptText); 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) { } catch (error: any) {
console.error( console.error(
`[Session ${this.clientId}] Failed to process audio for agent ${agentId}:`, `[Session ${this.clientId}] Failed to process audio for agent ${agentId}:`,
@@ -652,9 +695,14 @@ export class Session {
/** /**
* Handle create agent request * Handle create agent request
*/ */
private async handleCreateAgentRequest(cwd: string, initialMode?: string): Promise<void> { private async handleCreateAgentRequest(
cwd: string,
initialMode?: string
): Promise<void> {
console.log( 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 { try {
@@ -666,10 +714,12 @@ export class Session {
console.log(`[Session ${this.clientId}] Created agent ${agentId}`); console.log(`[Session ${this.clientId}] Created agent ${agentId}`);
// Get agent info // 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:`, { console.log(`[Session ${this.clientId}] Agent info:`, {
currentModeId: agentInfo?.currentModeId, currentModeId: agentInfo?.currentModeId,
availableModes: agentInfo?.availableModes availableModes: agentInfo?.availableModes,
}); });
// Subscribe to agent updates // Subscribe to agent updates
@@ -688,7 +738,10 @@ export class Session {
cwd: agentInfo?.cwd || cwd, 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) { } catch (error: any) {
console.error( console.error(
`[Session ${this.clientId}] Failed to create agent:`, `[Session ${this.clientId}] Failed to create agent:`,
@@ -710,15 +763,22 @@ export class Session {
/** /**
* Handle set agent mode request * Handle set agent mode request
*/ */
private async handleSetAgentMode(agentId: string, modeId: string): Promise<void> { private async handleSetAgentMode(
console.log(`[Session ${this.clientId}] Setting agent ${agentId} mode to ${modeId}`); agentId: string,
modeId: string
): Promise<void> {
console.log(
`[Session ${this.clientId}] Setting agent ${agentId} mode to ${modeId}`
);
try { try {
await this.agentManager.setSessionMode(agentId, modeId); 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 // 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) { if (info) {
this.emit({ this.emit({
type: "agent_status", type: "agent_status",
@@ -741,7 +801,10 @@ export class Session {
}); });
} }
} catch (error: any) { } 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({ this.emit({
type: "activity_log", type: "activity_log",
payload: { payload: {
@@ -758,14 +821,25 @@ export class Session {
/** /**
* Handle agent permission response from user * Handle agent permission response from user
*/ */
private async handleAgentPermissionResponse(agentId: string, requestId: string, optionId: string): Promise<void> { private async handleAgentPermissionResponse(
console.log(`[Session ${this.clientId}] Handling permission response for agent ${agentId}, request ${requestId}, option ${optionId}`); agentId: string,
requestId: string,
optionId: string
): Promise<void> {
console.log(
`[Session ${this.clientId}] Handling permission response for agent ${agentId}, request ${requestId}, option ${optionId}`
);
try { try {
this.agentManager.respondToPermission(agentId, requestId, optionId); 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) { } 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({ this.emit({
type: "activity_log", type: "activity_log",
payload: { payload: {
@@ -803,7 +877,10 @@ export class Session {
type: "agent_update", type: "agent_update",
payload: { payload: {
agentId: update.agentId, 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, notification: update.notification,
}, },
}); });
@@ -1123,6 +1200,11 @@ export class Session {
const result = await streamText({ const result = await streamText({
model: openrouter("anthropic/claude-haiku-4.5"), model: openrouter("anthropic/claude-haiku-4.5"),
system: getSystemPrompt(), system: getSystemPrompt(),
providerOptions: {
openrouter: {
transforms: ["middle-out"], // Compress prompts that are > context size.
} as OpenRouterProviderOptions,
},
messages: this.messages, messages: this.messages,
tools: allTools, tools: allTools,
abortSignal: this.abortController.signal, abortSignal: this.abortController.signal,
@@ -1203,7 +1285,9 @@ export class Session {
this.subscribeToAgent(agentId); this.subscribeToAgent(agentId);
// Get full agent info and emit agent_created message // 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) { if (agentInfo) {
this.emit({ this.emit({
type: "agent_created", type: "agent_created",
@@ -1558,7 +1642,9 @@ export class Session {
for (const [agentId, unsubscribe] of this.agentUpdateUnsubscribers) { for (const [agentId, unsubscribe] of this.agentUpdateUnsubscribers) {
try { try {
unsubscribe(); unsubscribe();
console.log(`[Session ${this.clientId}] Unsubscribed from agent ${agentId}`); console.log(
`[Session ${this.clientId}] Unsubscribed from agent ${agentId}`
);
} catch (error) { } catch (error) {
console.error( console.error(
`[Session ${this.clientId}] Failed to unsubscribe from agent ${agentId}:`, `[Session ${this.clientId}] Failed to unsubscribe from agent ${agentId}:`,
@@ -2,6 +2,7 @@ import { generateObject } from "ai";
import { createOpenAI } from "@ai-sdk/openai"; import { createOpenAI } from "@ai-sdk/openai";
import { z } from "zod"; import { z } from "zod";
import type { AgentUpdate } from "../server/acp/types.js"; import type { AgentUpdate } from "../server/acp/types.js";
import { curateAgentActivity } from "../server/acp/activity-curator.js";
let openai: ReturnType<typeof createOpenAI> | null = null; let openai: ReturnType<typeof createOpenAI> | null = null;
@@ -14,48 +15,6 @@ export function isTitleGeneratorInitialized(): boolean {
return openai !== null; 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 * Generate a concise title for an agent based on its activity
* Returns a 3-5 word title similar to ChatGPT/Claude.ai * Returns a 3-5 word title similar to ChatGPT/Claude.ai
@@ -72,9 +31,9 @@ export async function generateAgentTitle(
return "New Agent"; 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"; return "New Agent";
} }
+2 -2
View File
@@ -14,11 +14,11 @@
"noUnusedParameters": true, "noUnusedParameters": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"outDir": "./dist/server", "outDir": "./dist/server",
"rootDir": "./src/server", "rootDir": "./src",
"declaration": true, "declaration": true,
"declarationMap": true, "declarationMap": true,
"sourceMap": true "sourceMap": true
}, },
"include": ["src/server/**/*"], "include": ["src/server/**/*", "src/services/**/*"],
"exclude": ["node_modules", "dist"] "exclude": ["node_modules", "dist"]
} }