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

View File

@@ -1785,6 +1785,7 @@ const styles = StyleSheet.create((theme) => ({
minHeight: 0,
},
scrollContent: {
paddingTop: theme.spacing[6],
paddingBottom: theme.spacing[4],
flexGrow: 1,
},

View File

@@ -53,7 +53,7 @@ export function AgentStreamView({
<ScrollView
ref={scrollViewRef}
style={stylesheet.scrollView}
contentContainerStyle={{ paddingTop: 16, paddingBottom: Math.max(insets.bottom, 32) }}
contentContainerStyle={{ paddingTop: 24, paddingBottom: Math.max(insets.bottom, 32) }}
>
{streamItems.length === 0 ? (
<View style={stylesheet.emptyState}>

View File

@@ -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]);

View File

@@ -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<typeof sessionUpdate, { sessionUpdate: 'agent_message_chunk' }>;
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<typeof sessionUpdate, { sessionUpdate: 'agent_thought_chunk' }>;
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<typeof sessionUpdate, { sessionUpdate: 'tool_call' | 'tool_call_update' }>;
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<typeof sessionUpdate, { sessionUpdate: 'plan' }>;
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<typeof sessionUpdate, { sessionUpdate: 'user_message_chunk' }>;
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());
}

View File

@@ -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

View File

@@ -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 {

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
@@ -15,6 +15,14 @@ export interface EnrichedSessionNotification extends Omit<SessionNotification, '
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
*/
@@ -32,7 +40,7 @@ export type AgentStatus =
export interface AgentInfo {
id: string;
status: AgentStatus;
createdAt: string;
createdAt: Date;
type: "claude";
sessionId: string | null;
error: string | null;
@@ -44,12 +52,12 @@ export interface AgentInfo {
/**
* Update from an agent session
* Wraps enriched SessionNotification with additional metadata
* Wraps all notification types with additional metadata
*/
export interface AgentUpdate {
agentId: string;
timestamp: Date;
notification: EnrichedSessionNotification;
notification: AgentNotification;
}
/**

View File

@@ -286,14 +286,9 @@ export const AgentPermissionRequestMessageSchema = z.object({
agentId: z.string(),
requestId: z.string(),
sessionId: z.string(),
toolCall: z.object({
toolCallId: z.string(),
rawInput: z.object({
plan: z.string(),
}),
}),
toolCall: z.any(), // ToolCallUpdate from ACP SDK - complex type with flexible rawInput
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(),
optionId: z.string(),
})),

View File

@@ -6,7 +6,10 @@ import { join } from "path";
import invariant from "tiny-invariant";
import { streamText, stepCountIs } from "ai";
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 {
SessionInboundMessage,
SessionOutboundMessage,
@@ -15,14 +18,21 @@ import { getSystemPrompt } from "./agent/system-prompt.js";
import { getAllTools } from "./agent/llm-openai.js";
import { TTSManager } from "./agent/tts-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 { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { createTerminalMcpServer } from "./terminal-mcp/index.js";
import { AgentManager } from "./acp/agent-manager.js";
import { createAgentMcpServer } from "./acp/mcp-server.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);
@@ -72,10 +82,14 @@ export class Session {
private readonly sttManager: STTManager;
// 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 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 agentManager: AgentManager;
private agentUpdateUnsubscribers: Map<string, () => 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<void> {
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<void> {
private async handleSendAgentMessage(
agentId: string,
text: string,
messageId?: string
): Promise<void> {
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<void> {
private async handleCreateAgentRequest(
cwd: string,
initialMode?: string
): Promise<void> {
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<void> {
console.log(`[Session ${this.clientId}] Setting agent ${agentId} mode to ${modeId}`);
private async handleSetAgentMode(
agentId: string,
modeId: string
): Promise<void> {
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<void> {
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<void> {
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}:`,

View File

@@ -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<typeof createOpenAI> | 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";
}

View File

@@ -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"]
}