mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
```
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:
@@ -1785,6 +1785,7 @@ const styles = StyleSheet.create((theme) => ({
|
||||
minHeight: 0,
|
||||
},
|
||||
scrollContent: {
|
||||
paddingTop: theme.spacing[6],
|
||||
paddingBottom: theme.spacing[4],
|
||||
flexGrow: 1,
|
||||
},
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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(),
|
||||
})),
|
||||
|
||||
@@ -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}:`,
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user