```
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,
|
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}>
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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(),
|
||||||
})),
|
})),
|
||||||
|
|||||||
@@ -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";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user