refactor(agent): consolidate agent type definitions and add multi-agent-type support
This commit is contained in:
@@ -61,7 +61,7 @@ function GroupedTextItem({ item }: { item: GroupedTextMessage }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[stylesheet.card, isThought && stylesheet.thoughtCard]}>
|
<View style={[stylesheet.card, isThought && stylesheet.thoughtCard]}>
|
||||||
<Text style={stylesheet.timestamp}>
|
<Text style={[stylesheet.timestamp, isThought && stylesheet.thoughtTimestamp]}>
|
||||||
{formatTimestamp(item.startTimestamp)}
|
{formatTimestamp(item.startTimestamp)}
|
||||||
</Text>
|
</Text>
|
||||||
{isThought && (
|
{isThought && (
|
||||||
@@ -260,17 +260,21 @@ const stylesheet = StyleSheet.create((theme) => ({
|
|||||||
backgroundColor: theme.colors.muted,
|
backgroundColor: theme.colors.muted,
|
||||||
borderLeftWidth: 3,
|
borderLeftWidth: 3,
|
||||||
borderLeftColor: theme.colors.palette.purple[500],
|
borderLeftColor: theme.colors.palette.purple[500],
|
||||||
|
paddingVertical: theme.spacing[2],
|
||||||
},
|
},
|
||||||
timestamp: {
|
timestamp: {
|
||||||
color: theme.colors.mutedForeground,
|
color: theme.colors.mutedForeground,
|
||||||
fontSize: theme.fontSize.xs,
|
fontSize: theme.fontSize.xs,
|
||||||
marginBottom: theme.spacing[1],
|
marginBottom: theme.spacing[1],
|
||||||
},
|
},
|
||||||
|
thoughtTimestamp: {
|
||||||
|
marginBottom: theme.spacing[0],
|
||||||
|
},
|
||||||
thoughtLabel: {
|
thoughtLabel: {
|
||||||
color: theme.colors.palette.purple[500],
|
color: theme.colors.palette.purple[500],
|
||||||
fontSize: theme.fontSize.xs,
|
fontSize: theme.fontSize.xs,
|
||||||
fontWeight: theme.fontWeight.semibold,
|
fontWeight: theme.fontWeight.semibold,
|
||||||
marginBottom: theme.spacing[1],
|
marginBottom: theme.spacing[0],
|
||||||
},
|
},
|
||||||
text: {
|
text: {
|
||||||
color: theme.colors.foreground,
|
color: theme.colors.foreground,
|
||||||
|
|||||||
@@ -19,18 +19,16 @@ import {
|
|||||||
UserMessage,
|
UserMessage,
|
||||||
ActivityLog,
|
ActivityLog,
|
||||||
ToolCall,
|
ToolCall,
|
||||||
|
AgentThoughtMessage,
|
||||||
} from "./message";
|
} from "./message";
|
||||||
import { ToolCallBottomSheet } from "./tool-call-bottom-sheet";
|
import { ToolCallBottomSheet } from "./tool-call-bottom-sheet";
|
||||||
import type { StreamItem } from "@/types/stream";
|
import type { StreamItem } from "@/types/stream";
|
||||||
import type {
|
import type { SelectedToolCall, PendingPermission } from "@/types/shared";
|
||||||
SelectedToolCall,
|
import type { Agent } from "@/contexts/session-context";
|
||||||
PendingPermission,
|
|
||||||
AgentInfo,
|
|
||||||
} from "@/types/shared";
|
|
||||||
|
|
||||||
export interface AgentStreamViewProps {
|
export interface AgentStreamViewProps {
|
||||||
agentId: string;
|
agentId: string;
|
||||||
agent: AgentInfo;
|
agent: Agent;
|
||||||
streamItems: StreamItem[];
|
streamItems: StreamItem[];
|
||||||
pendingPermissions: Map<string, PendingPermission>;
|
pendingPermissions: Map<string, PendingPermission>;
|
||||||
onPermissionResponse: (requestId: string, optionId: string) => void;
|
onPermissionResponse: (requestId: string, optionId: string) => void;
|
||||||
@@ -53,6 +51,7 @@ export function AgentStreamView({
|
|||||||
const hasAutoScrolledOnce = useRef(false);
|
const hasAutoScrolledOnce = useRef(false);
|
||||||
const isProgrammaticScrollRef = useRef(false);
|
const isProgrammaticScrollRef = useRef(false);
|
||||||
const isNearBottomRef = useRef(true);
|
const isNearBottomRef = useRef(true);
|
||||||
|
const isUserScrollingRef = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
hasScrolledInitially.current = false;
|
hasScrolledInitially.current = false;
|
||||||
@@ -77,12 +76,41 @@ export function AgentStreamView({
|
|||||||
const { contentOffset } = event.nativeEvent;
|
const { contentOffset } = event.nativeEvent;
|
||||||
const threshold = Math.max(insets.bottom, 32);
|
const threshold = Math.max(insets.bottom, 32);
|
||||||
const nearBottom = contentOffset.y <= threshold;
|
const nearBottom = contentOffset.y <= threshold;
|
||||||
|
|
||||||
|
if (isProgrammaticScrollRef.current) {
|
||||||
|
if (nearBottom && !isNearBottomRef.current) {
|
||||||
|
isNearBottomRef.current = true;
|
||||||
|
setIsNearBottom(true);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nearBottom && !isUserScrollingRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNearBottomRef.current === nearBottom) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
isNearBottomRef.current = nearBottom;
|
isNearBottomRef.current = nearBottom;
|
||||||
setIsNearBottom(nearBottom);
|
setIsNearBottom(nearBottom);
|
||||||
},
|
},
|
||||||
[insets.bottom]
|
[insets.bottom]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleScrollBeginDrag = useCallback(() => {
|
||||||
|
isUserScrollingRef.current = true;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleMomentumScrollBegin = useCallback(() => {
|
||||||
|
isUserScrollingRef.current = true;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleScrollEnd = useCallback(() => {
|
||||||
|
isUserScrollingRef.current = false;
|
||||||
|
}, []);
|
||||||
|
|
||||||
const scrollToBottomInternal = useCallback(
|
const scrollToBottomInternal = useCallback(
|
||||||
({ animated }: { animated: boolean }) => {
|
({ animated }: { animated: boolean }) => {
|
||||||
const list = flatListRef.current;
|
const list = flatListRef.current;
|
||||||
@@ -156,10 +184,8 @@ export function AgentStreamView({
|
|||||||
|
|
||||||
case "thought":
|
case "thought":
|
||||||
return (
|
return (
|
||||||
<ActivityLog
|
<AgentThoughtMessage
|
||||||
type="info"
|
|
||||||
message={item.text}
|
message={item.text}
|
||||||
timestamp={item.timestamp.getTime()}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -235,6 +261,10 @@ export function AgentStreamView({
|
|||||||
}}
|
}}
|
||||||
style={stylesheet.list}
|
style={stylesheet.list}
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
|
onScrollBeginDrag={handleScrollBeginDrag}
|
||||||
|
onScrollEndDrag={handleScrollEnd}
|
||||||
|
onMomentumScrollBegin={handleMomentumScrollBegin}
|
||||||
|
onMomentumScrollEnd={handleScrollEnd}
|
||||||
scrollEventThrottle={16}
|
scrollEventThrottle={16}
|
||||||
ListEmptyComponent={
|
ListEmptyComponent={
|
||||||
<View style={stylesheet.invertedWrapper}>
|
<View style={stylesheet.invertedWrapper}>
|
||||||
|
|||||||
@@ -27,24 +27,30 @@ import { useRecentPaths } from "@/hooks/use-recent-paths";
|
|||||||
import { useSession } from "@/contexts/session-context";
|
import { useSession } from "@/contexts/session-context";
|
||||||
import { useRouter } from "expo-router";
|
import { useRouter } from "expo-router";
|
||||||
import { generateMessageId } from "@/types/stream";
|
import { generateMessageId } from "@/types/stream";
|
||||||
|
import {
|
||||||
|
listAgentTypeDefinitions,
|
||||||
|
type AgentType,
|
||||||
|
type AgentTypeDefinition,
|
||||||
|
} from "@server/server/acp/agent-types";
|
||||||
|
|
||||||
interface CreateAgentModalProps {
|
interface CreateAgentModalProps {
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MODES = [
|
const agentTypeDefinitions = listAgentTypeDefinitions();
|
||||||
{
|
|
||||||
value: "plan",
|
const agentTypeDefinitionMap: Record<AgentType, AgentTypeDefinition> =
|
||||||
label: "Plan",
|
{} as Record<AgentType, AgentTypeDefinition>;
|
||||||
description: "Plan and design before implementing",
|
for (const definition of agentTypeDefinitions) {
|
||||||
},
|
agentTypeDefinitionMap[definition.id] = definition;
|
||||||
{
|
}
|
||||||
value: "bypassPermissions",
|
|
||||||
label: "Bypass Permissions",
|
const fallbackDefinition = agentTypeDefinitionMap.claude ?? agentTypeDefinitions[0];
|
||||||
description: "Skip permission prompts for faster execution",
|
const DEFAULT_AGENT_TYPE: AgentType = fallbackDefinition
|
||||||
},
|
? fallbackDefinition.id
|
||||||
] as const;
|
: "claude";
|
||||||
|
const DEFAULT_MODE_FOR_DEFAULT_AGENT = fallbackDefinition?.defaultModeId;
|
||||||
|
|
||||||
export function CreateAgentModal({
|
export function CreateAgentModal({
|
||||||
isVisible,
|
isVisible,
|
||||||
@@ -58,11 +64,41 @@ export function CreateAgentModal({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const [workingDir, setWorkingDir] = useState("");
|
const [workingDir, setWorkingDir] = useState("");
|
||||||
const [selectedMode, setSelectedMode] = useState("plan");
|
const [selectedAgentType, setSelectedAgentType] = useState<AgentType>(
|
||||||
|
DEFAULT_AGENT_TYPE
|
||||||
|
);
|
||||||
|
const [selectedMode, setSelectedMode] = useState(
|
||||||
|
DEFAULT_MODE_FOR_DEFAULT_AGENT ?? ""
|
||||||
|
);
|
||||||
const [worktreeName, setWorktreeName] = useState("");
|
const [worktreeName, setWorktreeName] = useState("");
|
||||||
const [errorMessage, setErrorMessage] = useState("");
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [pendingRequestId, setPendingRequestId] = useState<string | null>(null);
|
const [pendingRequestId, setPendingRequestId] = useState<string | null>(null);
|
||||||
|
const agentDefinition = agentTypeDefinitionMap[selectedAgentType];
|
||||||
|
const modeOptions = agentDefinition?.availableModes ?? [];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!agentDefinition) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const availableModeIds = agentDefinition.availableModes.map(
|
||||||
|
(mode) => mode.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (availableModeIds.length === 0) {
|
||||||
|
if (selectedMode !== "") {
|
||||||
|
setSelectedMode("");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!availableModeIds.includes(selectedMode)) {
|
||||||
|
const fallbackModeId =
|
||||||
|
agentDefinition.defaultModeId ?? availableModeIds[0];
|
||||||
|
setSelectedMode(fallbackModeId);
|
||||||
|
}
|
||||||
|
}, [agentDefinition, selectedMode]);
|
||||||
|
|
||||||
// Use ref instead of state to survive state resets in onDismiss
|
// Use ref instead of state to survive state resets in onDismiss
|
||||||
const pendingNavigationAgentIdRef = useRef<string | null>(null);
|
const pendingNavigationAgentIdRef = useRef<string | null>(null);
|
||||||
@@ -164,11 +200,15 @@ export function CreateAgentModal({
|
|||||||
setPendingRequestId(requestId);
|
setPendingRequestId(requestId);
|
||||||
setErrorMessage("");
|
setErrorMessage("");
|
||||||
|
|
||||||
|
const modeId =
|
||||||
|
modeOptions.length > 0 && selectedMode !== "" ? selectedMode : undefined;
|
||||||
|
|
||||||
// Create the agent
|
// Create the agent
|
||||||
try {
|
try {
|
||||||
createAgent({
|
createAgent({
|
||||||
cwd: path,
|
cwd: path,
|
||||||
initialMode: selectedMode,
|
agentType: selectedAgentType,
|
||||||
|
initialMode: modeId,
|
||||||
worktreeName: worktree || undefined,
|
worktreeName: worktree || undefined,
|
||||||
requestId,
|
requestId,
|
||||||
});
|
});
|
||||||
@@ -178,7 +218,17 @@ export function CreateAgentModal({
|
|||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setPendingRequestId(null);
|
setPendingRequestId(null);
|
||||||
}
|
}
|
||||||
}, [workingDir, worktreeName, selectedMode, isLoading, validateWorktreeName, addRecentPath, createAgent]);
|
}, [
|
||||||
|
workingDir,
|
||||||
|
worktreeName,
|
||||||
|
selectedMode,
|
||||||
|
selectedAgentType,
|
||||||
|
modeOptions,
|
||||||
|
isLoading,
|
||||||
|
validateWorktreeName,
|
||||||
|
addRecentPath,
|
||||||
|
createAgent,
|
||||||
|
]);
|
||||||
|
|
||||||
const renderFooter = useCallback(
|
const renderFooter = useCallback(
|
||||||
(props: BottomSheetFooterProps) => (
|
(props: BottomSheetFooterProps) => (
|
||||||
@@ -254,7 +304,8 @@ export function CreateAgentModal({
|
|||||||
// Reset all state
|
// Reset all state
|
||||||
setWorkingDir("");
|
setWorkingDir("");
|
||||||
setWorktreeName("");
|
setWorktreeName("");
|
||||||
setSelectedMode("plan");
|
setSelectedAgentType(DEFAULT_AGENT_TYPE);
|
||||||
|
setSelectedMode(DEFAULT_MODE_FOR_DEFAULT_AGENT ?? "");
|
||||||
setErrorMessage("");
|
setErrorMessage("");
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setPendingRequestId(null);
|
setPendingRequestId(null);
|
||||||
@@ -338,6 +389,58 @@ export function CreateAgentModal({
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Agent Type Selector */}
|
||||||
|
<View style={styles.formSection}>
|
||||||
|
<Text style={styles.label}>Agent Type</Text>
|
||||||
|
<View style={styles.agentTypeContainer}>
|
||||||
|
{agentTypeDefinitions.map((definition) => {
|
||||||
|
const isSelected = selectedAgentType === definition.id;
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
key={definition.id}
|
||||||
|
onPress={() => setSelectedAgentType(definition.id)}
|
||||||
|
disabled={isLoading}
|
||||||
|
style={[
|
||||||
|
styles.agentTypeOption,
|
||||||
|
isSelected && styles.agentTypeOptionSelected,
|
||||||
|
isLoading && styles.agentTypeOptionDisabled,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<View style={styles.agentTypeOptionContent}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.radioOuter,
|
||||||
|
isSelected
|
||||||
|
? styles.radioOuterSelected
|
||||||
|
: styles.radioOuterUnselected,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{isSelected && <View style={styles.radioInner} />}
|
||||||
|
</View>
|
||||||
|
<View style={styles.agentTypeTextContainer}>
|
||||||
|
<Text style={styles.agentTypeLabel}>
|
||||||
|
{definition.label}
|
||||||
|
</Text>
|
||||||
|
{definition.description ? (
|
||||||
|
<Text style={styles.agentTypeDescription}>
|
||||||
|
{definition.description}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
<Text style={styles.agentTypeMeta}>
|
||||||
|
{definition.availableModes.length > 0
|
||||||
|
? `Modes: ${definition.availableModes
|
||||||
|
.map((mode) => mode.name)
|
||||||
|
.join(", ")}`
|
||||||
|
: "Modes: none"}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
{/* Worktree Name Input (Optional) */}
|
{/* Worktree Name Input (Optional) */}
|
||||||
<View style={styles.formSection}>
|
<View style={styles.formSection}>
|
||||||
<Text style={styles.label}>Worktree Name (Optional)</Text>
|
<Text style={styles.label}>Worktree Name (Optional)</Text>
|
||||||
@@ -364,41 +467,50 @@ export function CreateAgentModal({
|
|||||||
{/* Mode Selector */}
|
{/* Mode Selector */}
|
||||||
<View style={styles.formSection}>
|
<View style={styles.formSection}>
|
||||||
<Text style={styles.label}>Mode</Text>
|
<Text style={styles.label}>Mode</Text>
|
||||||
<View style={styles.modeContainer}>
|
{modeOptions.length === 0 ? (
|
||||||
{MODES.map((mode) => (
|
<Text style={styles.helperText}>
|
||||||
<Pressable
|
This agent type does not expose selectable modes.
|
||||||
key={mode.value}
|
</Text>
|
||||||
onPress={() => setSelectedMode(mode.value)}
|
) : (
|
||||||
disabled={isLoading}
|
<View style={styles.modeContainer}>
|
||||||
style={[
|
{modeOptions.map((mode) => {
|
||||||
styles.modeOption,
|
const isSelected = selectedMode === mode.id;
|
||||||
selectedMode === mode.value && styles.modeOptionSelected,
|
return (
|
||||||
isLoading && styles.modeOptionDisabled,
|
<Pressable
|
||||||
]}
|
key={mode.id}
|
||||||
>
|
onPress={() => setSelectedMode(mode.id)}
|
||||||
<View style={styles.modeOptionContent}>
|
disabled={isLoading}
|
||||||
<View
|
|
||||||
style={[
|
style={[
|
||||||
styles.radioOuter,
|
styles.modeOption,
|
||||||
selectedMode === mode.value
|
isSelected && styles.modeOptionSelected,
|
||||||
? styles.radioOuterSelected
|
isLoading && styles.modeOptionDisabled,
|
||||||
: styles.radioOuterUnselected,
|
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{selectedMode === mode.value && (
|
<View style={styles.modeOptionContent}>
|
||||||
<View style={styles.radioInner} />
|
<View
|
||||||
)}
|
style={[
|
||||||
</View>
|
styles.radioOuter,
|
||||||
<View style={styles.modeTextContainer}>
|
isSelected
|
||||||
<Text style={styles.modeLabel}>{mode.label}</Text>
|
? styles.radioOuterSelected
|
||||||
<Text style={styles.modeDescription}>
|
: styles.radioOuterUnselected,
|
||||||
{mode.description}
|
]}
|
||||||
</Text>
|
>
|
||||||
</View>
|
{isSelected && <View style={styles.radioInner} />}
|
||||||
</View>
|
</View>
|
||||||
</Pressable>
|
<View style={styles.modeTextContainer}>
|
||||||
))}
|
<Text style={styles.modeLabel}>{mode.name}</Text>
|
||||||
</View>
|
{mode.description ? (
|
||||||
|
<Text style={styles.modeDescription}>
|
||||||
|
{mode.description}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</BottomSheetScrollView>
|
</BottomSheetScrollView>
|
||||||
</BottomSheetModal>
|
</BottomSheetModal>
|
||||||
@@ -516,6 +628,45 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
color: theme.colors.mutedForeground,
|
color: theme.colors.mutedForeground,
|
||||||
fontSize: theme.fontSize.sm,
|
fontSize: theme.fontSize.sm,
|
||||||
},
|
},
|
||||||
|
agentTypeContainer: {
|
||||||
|
gap: theme.spacing[3],
|
||||||
|
},
|
||||||
|
agentTypeOption: {
|
||||||
|
backgroundColor: theme.colors.background,
|
||||||
|
borderRadius: theme.borderRadius.lg,
|
||||||
|
borderWidth: theme.borderWidth[1],
|
||||||
|
borderColor: theme.colors.border,
|
||||||
|
padding: theme.spacing[4],
|
||||||
|
},
|
||||||
|
agentTypeOptionSelected: {
|
||||||
|
borderColor: theme.colors.palette.blue[500],
|
||||||
|
backgroundColor: theme.colors.muted,
|
||||||
|
},
|
||||||
|
agentTypeOptionDisabled: {
|
||||||
|
opacity: theme.opacity[50],
|
||||||
|
},
|
||||||
|
agentTypeOptionContent: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
agentTypeTextContainer: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
agentTypeLabel: {
|
||||||
|
color: theme.colors.foreground,
|
||||||
|
fontSize: theme.fontSize.base,
|
||||||
|
fontWeight: theme.fontWeight.semibold,
|
||||||
|
marginBottom: theme.spacing[1],
|
||||||
|
},
|
||||||
|
agentTypeDescription: {
|
||||||
|
color: theme.colors.mutedForeground,
|
||||||
|
fontSize: theme.fontSize.sm,
|
||||||
|
marginBottom: theme.spacing[1],
|
||||||
|
},
|
||||||
|
agentTypeMeta: {
|
||||||
|
color: theme.colors.mutedForeground,
|
||||||
|
fontSize: theme.fontSize.sm,
|
||||||
|
},
|
||||||
footer: {
|
footer: {
|
||||||
borderTopWidth: theme.borderWidth[1],
|
borderTopWidth: theme.borderWidth[1],
|
||||||
borderTopColor: theme.colors.border,
|
borderTopColor: theme.colors.border,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { View, Text, Pressable, Animated } from "react-native";
|
import { View, Text, Pressable, Animated } from "react-native";
|
||||||
import { useState, useEffect, useRef, memo } from "react";
|
import { useState, useEffect, useRef, memo, useMemo } from "react";
|
||||||
import Markdown from "react-native-markdown-display";
|
import Markdown from "react-native-markdown-display";
|
||||||
import {
|
import {
|
||||||
Circle,
|
Circle,
|
||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
Pencil,
|
Pencil,
|
||||||
Eye,
|
Eye,
|
||||||
SquareTerminal,
|
SquareTerminal,
|
||||||
|
Brain,
|
||||||
} from "lucide-react-native";
|
} from "lucide-react-native";
|
||||||
import { StyleSheet } from "react-native-unistyles";
|
import { StyleSheet } from "react-native-unistyles";
|
||||||
import { baseColors, theme } from "@/styles/theme";
|
import { baseColors, theme } from "@/styles/theme";
|
||||||
@@ -48,7 +49,10 @@ const userMessageStylesheet = StyleSheet.create((theme) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const UserMessage = memo(function UserMessage({ message, timestamp }: UserMessageProps) {
|
export const UserMessage = memo(function UserMessage({
|
||||||
|
message,
|
||||||
|
timestamp,
|
||||||
|
}: UserMessageProps) {
|
||||||
return (
|
return (
|
||||||
<View style={userMessageStylesheet.container}>
|
<View style={userMessageStylesheet.container}>
|
||||||
<View style={userMessageStylesheet.bubble}>
|
<View style={userMessageStylesheet.bubble}>
|
||||||
@@ -64,7 +68,7 @@ interface AssistantMessageProps {
|
|||||||
isStreaming?: boolean;
|
isStreaming?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const assistantMessageStylesheet = StyleSheet.create((theme) => ({
|
export const assistantMessageStylesheet = StyleSheet.create((theme) => ({
|
||||||
container: {
|
container: {
|
||||||
marginBottom: theme.spacing[3],
|
marginBottom: theme.spacing[3],
|
||||||
paddingHorizontal: theme.spacing[4],
|
paddingHorizontal: theme.spacing[4],
|
||||||
@@ -133,6 +137,20 @@ const assistantMessageStylesheet = StyleSheet.create((theme) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const markdownStyles = {
|
||||||
|
body: assistantMessageStylesheet.markdownBody,
|
||||||
|
paragraph: assistantMessageStylesheet.markdownParagraph,
|
||||||
|
strong: assistantMessageStylesheet.markdownStrong,
|
||||||
|
em: assistantMessageStylesheet.markdownEm,
|
||||||
|
code_inline: assistantMessageStylesheet.markdownCodeInline,
|
||||||
|
code_block: assistantMessageStylesheet.markdownCodeBlock,
|
||||||
|
fence: assistantMessageStylesheet.markdownFence,
|
||||||
|
link: assistantMessageStylesheet.markdownLink,
|
||||||
|
bullet_list: assistantMessageStylesheet.markdownList,
|
||||||
|
ordered_list: assistantMessageStylesheet.markdownList,
|
||||||
|
list_item: assistantMessageStylesheet.markdownListItem,
|
||||||
|
};
|
||||||
|
|
||||||
export const AssistantMessage = memo(function AssistantMessage({
|
export const AssistantMessage = memo(function AssistantMessage({
|
||||||
message,
|
message,
|
||||||
timestamp,
|
timestamp,
|
||||||
@@ -162,20 +180,6 @@ export const AssistantMessage = memo(function AssistantMessage({
|
|||||||
}
|
}
|
||||||
}, [isStreaming, fadeAnim]);
|
}, [isStreaming, fadeAnim]);
|
||||||
|
|
||||||
const markdownStyles = {
|
|
||||||
body: assistantMessageStylesheet.markdownBody,
|
|
||||||
paragraph: assistantMessageStylesheet.markdownParagraph,
|
|
||||||
strong: assistantMessageStylesheet.markdownStrong,
|
|
||||||
em: assistantMessageStylesheet.markdownEm,
|
|
||||||
code_inline: assistantMessageStylesheet.markdownCodeInline,
|
|
||||||
code_block: assistantMessageStylesheet.markdownCodeBlock,
|
|
||||||
fence: assistantMessageStylesheet.markdownFence,
|
|
||||||
link: assistantMessageStylesheet.markdownLink,
|
|
||||||
bullet_list: assistantMessageStylesheet.markdownList,
|
|
||||||
ordered_list: assistantMessageStylesheet.markdownList,
|
|
||||||
list_item: assistantMessageStylesheet.markdownListItem,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={assistantMessageStylesheet.container}>
|
<View style={assistantMessageStylesheet.container}>
|
||||||
<Markdown style={markdownStyles}>{message}</Markdown>
|
<Markdown style={markdownStyles}>{message}</Markdown>
|
||||||
@@ -376,6 +380,79 @@ export const ActivityLog = memo(function ActivityLog({
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
interface AgentThoughtMessageProps {
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const agentThoughtStylesheet = StyleSheet.create((theme) => ({
|
||||||
|
container: {
|
||||||
|
marginHorizontal: theme.spacing[2],
|
||||||
|
marginBottom: theme.spacing[2],
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
backgroundColor: theme.colors.card,
|
||||||
|
borderRadius: theme.borderRadius.lg,
|
||||||
|
borderWidth: theme.borderWidth[1],
|
||||||
|
borderColor: theme.colors.border,
|
||||||
|
paddingVertical: theme.spacing[2],
|
||||||
|
paddingHorizontal: theme.spacing[3],
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
gap: theme.spacing[3],
|
||||||
|
},
|
||||||
|
iconContainer: {
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
borderRadius: 14,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
flexShrink: 0,
|
||||||
|
paddingTop: theme.spacing[1],
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
color: theme.colors.mutedForeground,
|
||||||
|
opacity: 0.8,
|
||||||
|
},
|
||||||
|
textContainer: {
|
||||||
|
flex: 1,
|
||||||
|
paddingVertical: theme.spacing[1],
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
color: theme.colors.mutedForeground,
|
||||||
|
fontSize: theme.fontSize.xs,
|
||||||
|
fontWeight: theme.fontWeight.semibold,
|
||||||
|
textTransform: "uppercase",
|
||||||
|
marginBottom: theme.spacing[1],
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
color: theme.colors.mutedForeground,
|
||||||
|
fontSize: theme.fontSize.base,
|
||||||
|
lineHeight: 20,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export function AgentThoughtMessage({ message }: AgentThoughtMessageProps) {
|
||||||
|
const messageText = useMemo(() => {
|
||||||
|
return message
|
||||||
|
.trim()
|
||||||
|
.replace(/^\*\*|\*\*$/g, "")
|
||||||
|
.trim();
|
||||||
|
}, [message]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={agentThoughtStylesheet.container}>
|
||||||
|
<View style={agentThoughtStylesheet.card}>
|
||||||
|
<View style={agentThoughtStylesheet.iconContainer}>
|
||||||
|
<Brain size={18} style={agentThoughtStylesheet.icon} />
|
||||||
|
</View>
|
||||||
|
<View style={agentThoughtStylesheet.textContainer}>
|
||||||
|
<Text style={agentThoughtStylesheet.text}>{messageText}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
interface ToolCallProps {
|
interface ToolCallProps {
|
||||||
toolName: string;
|
toolName: string;
|
||||||
kind?: string; // Optional kind for ACP tool calls
|
kind?: string; // Optional kind for ACP tool calls
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type {
|
|||||||
WSInboundMessage,
|
WSInboundMessage,
|
||||||
} from "@server/server/messages";
|
} from "@server/server/messages";
|
||||||
import type { AgentStatus, AgentUpdate, AgentNotification } from "@server/server/acp/types";
|
import type { AgentStatus, AgentUpdate, AgentNotification } from "@server/server/acp/types";
|
||||||
|
import type { AgentType } from "@server/server/acp/agent-types";
|
||||||
import { parseSessionUpdate } from "@/types/agent-activity";
|
import { parseSessionUpdate } from "@/types/agent-activity";
|
||||||
import { ScrollView } from "react-native";
|
import { ScrollView } from "react-native";
|
||||||
import * as FileSystem from 'expo-file-system';
|
import * as FileSystem from 'expo-file-system';
|
||||||
@@ -58,7 +59,7 @@ export interface Agent {
|
|||||||
status: AgentStatus;
|
status: AgentStatus;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
lastActivityAt: Date;
|
lastActivityAt: Date;
|
||||||
type: "claude";
|
type: AgentType;
|
||||||
sessionId: string | null;
|
sessionId: string | null;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
currentModeId: string | null;
|
currentModeId: string | null;
|
||||||
@@ -118,7 +119,13 @@ interface SessionContextValue {
|
|||||||
initializeAgent: (params: { agentId: string; requestId?: string }) => void;
|
initializeAgent: (params: { agentId: string; requestId?: string }) => void;
|
||||||
sendAgentMessage: (agentId: string, message: string, imageUris?: string[]) => Promise<void>;
|
sendAgentMessage: (agentId: string, message: string, imageUris?: string[]) => Promise<void>;
|
||||||
sendAgentAudio: (agentId: string, audioBlob: Blob, requestId?: string) => Promise<void>;
|
sendAgentAudio: (agentId: string, audioBlob: Blob, requestId?: string) => Promise<void>;
|
||||||
createAgent: (options: { cwd: string; initialMode?: string; worktreeName?: string; requestId?: string }) => void;
|
createAgent: (options: {
|
||||||
|
cwd: string;
|
||||||
|
agentType: AgentType;
|
||||||
|
initialMode?: string;
|
||||||
|
worktreeName?: string;
|
||||||
|
requestId?: string;
|
||||||
|
}) => void;
|
||||||
setAgentMode: (agentId: string, modeId: string) => void;
|
setAgentMode: (agentId: string, modeId: string) => void;
|
||||||
respondToPermission: (requestId: string, agentId: string, sessionId: string, selectedOptionIds: string[]) => void;
|
respondToPermission: (requestId: string, agentId: string, sessionId: string, selectedOptionIds: string[]) => void;
|
||||||
}
|
}
|
||||||
@@ -720,7 +727,7 @@ export function SessionProvider({ children, serverUrl }: SessionProviderProps) {
|
|||||||
}
|
}
|
||||||
}, [ws]);
|
}, [ws]);
|
||||||
|
|
||||||
const createAgent = useCallback((options: { cwd: string; initialMode?: string; worktreeName?: string; requestId?: string }) => {
|
const createAgent = useCallback((options: { cwd: string; agentType: AgentType; initialMode?: string; worktreeName?: string; requestId?: string }) => {
|
||||||
const msg: WSInboundMessage = {
|
const msg: WSInboundMessage = {
|
||||||
type: "session",
|
type: "session",
|
||||||
message: {
|
message: {
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
export type AgentType = "claude" | "codex";
|
|
||||||
|
|
||||||
export interface AgentModeDefinition {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AgentTypeDefinition {
|
|
||||||
id: AgentType;
|
|
||||||
label: string;
|
|
||||||
spawn: {
|
|
||||||
command: string;
|
|
||||||
args: string[];
|
|
||||||
};
|
|
||||||
supportsSessionPersistence: boolean;
|
|
||||||
availableModes: AgentModeDefinition[];
|
|
||||||
defaultModeId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const agentTypeDefinitions: Record<AgentType, AgentTypeDefinition> = {
|
|
||||||
claude: {
|
|
||||||
id: "claude",
|
|
||||||
label: "Claude Code",
|
|
||||||
spawn: {
|
|
||||||
command: "npx",
|
|
||||||
args: ["@boudra/claude-code-acp"],
|
|
||||||
},
|
|
||||||
supportsSessionPersistence: true,
|
|
||||||
availableModes: [
|
|
||||||
{
|
|
||||||
id: "plan",
|
|
||||||
name: "Plan",
|
|
||||||
description: "Plan and design before implementing",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "bypassPermissions",
|
|
||||||
name: "Bypass Permissions",
|
|
||||||
description: "Skip permission prompts for faster execution",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
defaultModeId: "plan",
|
|
||||||
},
|
|
||||||
codex: {
|
|
||||||
id: "codex",
|
|
||||||
label: "Codex",
|
|
||||||
spawn: {
|
|
||||||
command: "npx",
|
|
||||||
args: ["@zed-industries/codex-acp"],
|
|
||||||
},
|
|
||||||
supportsSessionPersistence: false,
|
|
||||||
availableModes: [
|
|
||||||
{
|
|
||||||
id: "default",
|
|
||||||
name: "Default",
|
|
||||||
description: "Standard coding mode",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
defaultModeId: "default",
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export function getAgentTypeDefinition(type: AgentType): AgentTypeDefinition {
|
|
||||||
return agentTypeDefinitions[type];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getAgentModes(type: AgentType): AgentModeDefinition[] {
|
|
||||||
return agentTypeDefinitions[type].availableModes;
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { AgentStatus } from '@server/server/acp/types';
|
|
||||||
import type { RequestPermissionRequest } from '@agentclientprotocol/sdk';
|
import type { RequestPermissionRequest } from '@agentclientprotocol/sdk';
|
||||||
import type { ToolCallPayload } from './stream';
|
import type { ToolCallPayload } from './stream';
|
||||||
|
|
||||||
@@ -22,12 +21,4 @@ export interface PendingPermission {
|
|||||||
options: RequestPermissionRequest['options'];
|
options: RequestPermissionRequest['options'];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Agent info interface is provided by SessionContext's Agent type
|
||||||
* Agent info for UI display
|
|
||||||
*/
|
|
||||||
export interface AgentInfo {
|
|
||||||
id: string;
|
|
||||||
status: AgentStatus;
|
|
||||||
createdAt: Date;
|
|
||||||
type: 'claude';
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -284,7 +284,7 @@
|
|||||||
"sessionId": "20b0b2c2-18ec-409b-b932-a20c9ca8e0d3"
|
"sessionId": "20b0b2c2-18ec-409b-b932-a20c9ca8e0d3"
|
||||||
},
|
},
|
||||||
"createdAt": "2025-10-26T09:45:29.563Z",
|
"createdAt": "2025-10-26T09:45:29.563Z",
|
||||||
"lastActivityAt": "2025-10-26T13:11:22.795Z",
|
"lastActivityAt": "2025-10-26T14:53:00.404Z",
|
||||||
"cwd": "/Users/moboudra/dev/voice-dev"
|
"cwd": "/Users/moboudra/dev/voice-dev"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -380,7 +380,18 @@
|
|||||||
"sessionId": "addbd42f-dbb1-4536-bc2b-e70a64587de6"
|
"sessionId": "addbd42f-dbb1-4536-bc2b-e70a64587de6"
|
||||||
},
|
},
|
||||||
"createdAt": "2025-10-26T14:02:59.844Z",
|
"createdAt": "2025-10-26T14:02:59.844Z",
|
||||||
"lastActivityAt": "2025-10-26T14:08:58.462Z",
|
"lastActivityAt": "2025-10-26T16:45:15.700Z",
|
||||||
|
"cwd": "/Users/moboudra/dev/voice-dev"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "f1df26aa-4b20-499e-a84b-1d61828cf7fe",
|
||||||
|
"title": "Prepare Concise Session Title",
|
||||||
|
"sessionId": "019a215a-22dd-7930-9db7-e38d256ac597",
|
||||||
|
"options": {
|
||||||
|
"type": "codex"
|
||||||
|
},
|
||||||
|
"createdAt": "2025-10-26T16:29:01.354Z",
|
||||||
|
"lastActivityAt": "2025-10-26T16:29:17.523Z",
|
||||||
"cwd": "/Users/moboudra/dev/voice-dev"
|
"cwd": "/Users/moboudra/dev/voice-dev"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
"@agentclientprotocol/sdk": "^0.4.9",
|
"@agentclientprotocol/sdk": "^0.4.9",
|
||||||
"@ai-sdk/openai": "^2.0.52",
|
"@ai-sdk/openai": "^2.0.52",
|
||||||
"@boudra/claude-code-acp": "^0.8.5",
|
"@boudra/claude-code-acp": "^0.8.5",
|
||||||
|
"@zed-industries/codex-acp": "^0.3.9",
|
||||||
"@deepgram/sdk": "^3.4.0",
|
"@deepgram/sdk": "^3.4.0",
|
||||||
"@modelcontextprotocol/sdk": "^1.20.1",
|
"@modelcontextprotocol/sdk": "^1.20.1",
|
||||||
"@openrouter/ai-sdk-provider": "^1.2.0",
|
"@openrouter/ai-sdk-provider": "^1.2.0",
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ describe("AgentManager", () => {
|
|||||||
|
|
||||||
const agentId = await manager.createAgent({
|
const agentId = await manager.createAgent({
|
||||||
cwd: tmpDir,
|
cwd: tmpDir,
|
||||||
|
type: "claude",
|
||||||
initialMode: "plan",
|
initialMode: "plan",
|
||||||
});
|
});
|
||||||
createdAgents.push({ manager, agentId });
|
createdAgents.push({ manager, agentId });
|
||||||
@@ -96,10 +97,11 @@ describe("AgentManager", () => {
|
|||||||
}, 120000);
|
}, 120000);
|
||||||
|
|
||||||
describe("persistence", () => {
|
describe("persistence", () => {
|
||||||
it.only("should load persisted agent and send new prompt", async () => {
|
it("should load persisted agent and send new prompt", async () => {
|
||||||
const manager = new AgentManager();
|
const manager = new AgentManager();
|
||||||
const agentId = await manager.createAgent({
|
const agentId = await manager.createAgent({
|
||||||
cwd: tmpDir,
|
cwd: tmpDir,
|
||||||
|
type: "claude",
|
||||||
});
|
});
|
||||||
createdAgents.push({ manager, agentId });
|
createdAgents.push({ manager, agentId });
|
||||||
|
|
||||||
@@ -115,7 +117,9 @@ describe("AgentManager", () => {
|
|||||||
|
|
||||||
expect(status).toBe("completed");
|
expect(status).toBe("completed");
|
||||||
|
|
||||||
const agentBeforeKill = manager.listAgents().find((a) => a.id === agentId);
|
const agentBeforeKill = manager
|
||||||
|
.listAgents()
|
||||||
|
.find((a) => a.id === agentId);
|
||||||
const claudeSessionId = manager.getClaudeSessionId(agentId);
|
const claudeSessionId = manager.getClaudeSessionId(agentId);
|
||||||
console.log("Before kill - ACP Session ID:", agentBeforeKill?.sessionId);
|
console.log("Before kill - ACP Session ID:", agentBeforeKill?.sessionId);
|
||||||
console.log("Before kill - Claude Session ID:", claudeSessionId);
|
console.log("Before kill - Claude Session ID:", claudeSessionId);
|
||||||
@@ -127,7 +131,9 @@ describe("AgentManager", () => {
|
|||||||
await newManager.initialize();
|
await newManager.initialize();
|
||||||
|
|
||||||
// Update the tracking to use the new manager instance
|
// Update the tracking to use the new manager instance
|
||||||
const trackingIndex = createdAgents.findIndex((a) => a.agentId === agentId);
|
const trackingIndex = createdAgents.findIndex(
|
||||||
|
(a) => a.agentId === agentId
|
||||||
|
);
|
||||||
if (trackingIndex >= 0) {
|
if (trackingIndex >= 0) {
|
||||||
createdAgents[trackingIndex] = { manager: newManager, agentId };
|
createdAgents[trackingIndex] = { manager: newManager, agentId };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ import {
|
|||||||
} from "@agentclientprotocol/sdk";
|
} from "@agentclientprotocol/sdk";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { expandTilde } from "../terminal-mcp/tmux.js";
|
import { expandTilde } from "../terminal-mcp/tmux.js";
|
||||||
|
import {
|
||||||
|
getAgentModes,
|
||||||
|
getAgentTypeDefinition,
|
||||||
|
} from "./agent-types.js";
|
||||||
import type {
|
import type {
|
||||||
AgentStatus,
|
AgentStatus,
|
||||||
AgentInfo,
|
AgentInfo,
|
||||||
@@ -86,6 +90,84 @@ function getAgentError(state: ManagedAgentState): string | undefined {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeModes(modes?: SessionMode[] | null): SessionMode[] {
|
||||||
|
if (!modes) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return modes.map((mode) => ({
|
||||||
|
id: mode.id,
|
||||||
|
name: mode.name,
|
||||||
|
description: mode.description ?? null,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStaticModes(type: AgentOptions["type"]): SessionMode[] {
|
||||||
|
const staticModes = getAgentModes(type);
|
||||||
|
return staticModes.map((mode) => ({
|
||||||
|
id: mode.id,
|
||||||
|
name: mode.name,
|
||||||
|
description: mode.description ?? null,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAvailableModes(
|
||||||
|
type: AgentOptions["type"],
|
||||||
|
runtimeModes?: SessionMode[] | null
|
||||||
|
): SessionMode[] | null {
|
||||||
|
const runtimeList = normalizeModes(runtimeModes);
|
||||||
|
if (runtimeList.length > 0) {
|
||||||
|
return runtimeList;
|
||||||
|
}
|
||||||
|
|
||||||
|
const staticModes = getStaticModes(type);
|
||||||
|
return staticModes.length > 0 ? staticModes : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveModeSelection({
|
||||||
|
type,
|
||||||
|
requestedModeId,
|
||||||
|
runtimeModes,
|
||||||
|
}: {
|
||||||
|
type: AgentOptions["type"];
|
||||||
|
requestedModeId?: string | null;
|
||||||
|
runtimeModes?: SessionMode[] | null;
|
||||||
|
}): {
|
||||||
|
availableModes: SessionMode[] | null;
|
||||||
|
modeId: string | null;
|
||||||
|
wasAdjusted: boolean;
|
||||||
|
} {
|
||||||
|
const availableModes = buildAvailableModes(type, runtimeModes);
|
||||||
|
|
||||||
|
if (!availableModes || availableModes.length === 0) {
|
||||||
|
return {
|
||||||
|
availableModes: null,
|
||||||
|
modeId: null,
|
||||||
|
wasAdjusted: Boolean(requestedModeId),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestedModeId) {
|
||||||
|
const match = availableModes.find((mode) => mode.id === requestedModeId);
|
||||||
|
if (match) {
|
||||||
|
return {
|
||||||
|
availableModes,
|
||||||
|
modeId: requestedModeId,
|
||||||
|
wasAdjusted: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const definition = getAgentTypeDefinition(type);
|
||||||
|
const fallbackModeId =
|
||||||
|
definition.defaultModeId ?? (availableModes[0]?.id ?? null);
|
||||||
|
|
||||||
|
return {
|
||||||
|
availableModes,
|
||||||
|
modeId: fallbackModeId,
|
||||||
|
wasAdjusted: Boolean(requestedModeId),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Client implementation for ACP callbacks
|
* Client implementation for ACP callbacks
|
||||||
*/
|
*/
|
||||||
@@ -236,10 +318,26 @@ export class AgentManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const createdAt = new Date();
|
const createdAt = new Date();
|
||||||
const agentOptions: AgentOptions = {
|
const agentOptions: AgentOptions =
|
||||||
type: "claude",
|
options.type === "claude"
|
||||||
sessionId: null,
|
? {
|
||||||
};
|
type: "claude",
|
||||||
|
sessionId: null,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
type: "codex",
|
||||||
|
};
|
||||||
|
|
||||||
|
const modeSelection = resolveModeSelection({
|
||||||
|
type: options.type,
|
||||||
|
requestedModeId: options.initialMode ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.initialMode && modeSelection.wasAdjusted) {
|
||||||
|
console.warn(
|
||||||
|
`[AgentManager] Invalid initial mode '${options.initialMode}' for agent type '${options.type}'. Falling back to '${modeSelection.modeId ?? "none"}'.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const agent: ManagedAgent = {
|
const agent: ManagedAgent = {
|
||||||
id: agentId,
|
id: agentId,
|
||||||
@@ -254,9 +352,7 @@ export class AgentManager {
|
|||||||
currentAssistantMessageId: null,
|
currentAssistantMessageId: null,
|
||||||
currentThoughtId: null,
|
currentThoughtId: null,
|
||||||
titleGenerationTriggered: false,
|
titleGenerationTriggered: false,
|
||||||
pendingSessionMode: options.initialPrompt
|
pendingSessionMode: options.initialPrompt ? null : modeSelection.modeId,
|
||||||
? null
|
|
||||||
: options.initialMode ?? null,
|
|
||||||
state: {
|
state: {
|
||||||
type: "uninitialized",
|
type: "uninitialized",
|
||||||
persistedSessionId: null,
|
persistedSessionId: null,
|
||||||
@@ -593,14 +689,17 @@ export class AgentManager {
|
|||||||
const runtime = this.getRuntime(agent);
|
const runtime = this.getRuntime(agent);
|
||||||
const sessionId = runtime?.sessionId ?? null;
|
const sessionId = runtime?.sessionId ?? null;
|
||||||
const currentModeId = runtime?.currentModeId ?? null;
|
const currentModeId = runtime?.currentModeId ?? null;
|
||||||
const availableModes = runtime?.availableModes ?? null;
|
const availableModes = buildAvailableModes(
|
||||||
|
agent.options.type,
|
||||||
|
runtime?.availableModes ?? null
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: agent.id,
|
id: agent.id,
|
||||||
status,
|
status,
|
||||||
createdAt: agent.createdAt,
|
createdAt: agent.createdAt,
|
||||||
lastActivityAt: agent.lastActivityAt,
|
lastActivityAt: agent.lastActivityAt,
|
||||||
type: "claude" as const,
|
type: agent.options.type,
|
||||||
sessionId,
|
sessionId,
|
||||||
error: error ?? null,
|
error: error ?? null,
|
||||||
currentModeId,
|
currentModeId,
|
||||||
@@ -659,17 +758,21 @@ export class AgentManager {
|
|||||||
const status = getAgentStatusFromState(agent.state);
|
const status = getAgentStatusFromState(agent.state);
|
||||||
const error = getAgentError(agent.state);
|
const error = getAgentError(agent.state);
|
||||||
const runtime = this.getRuntime(agent);
|
const runtime = this.getRuntime(agent);
|
||||||
|
const availableModes = buildAvailableModes(
|
||||||
|
agent.options.type,
|
||||||
|
runtime?.availableModes ?? null
|
||||||
|
);
|
||||||
|
|
||||||
const info: AgentInfo = {
|
const info: AgentInfo = {
|
||||||
id: agent.id,
|
id: agent.id,
|
||||||
status,
|
status,
|
||||||
createdAt: agent.createdAt,
|
createdAt: agent.createdAt,
|
||||||
lastActivityAt: agent.lastActivityAt,
|
lastActivityAt: agent.lastActivityAt,
|
||||||
type: "claude",
|
type: agent.options.type,
|
||||||
sessionId: runtime?.sessionId ?? null,
|
sessionId: runtime?.sessionId ?? null,
|
||||||
error: error ?? null,
|
error: error ?? null,
|
||||||
currentModeId: runtime?.currentModeId ?? null,
|
currentModeId: runtime?.currentModeId ?? null,
|
||||||
availableModes: runtime?.availableModes ?? null,
|
availableModes,
|
||||||
title: agent.title,
|
title: agent.title,
|
||||||
cwd: agent.cwd,
|
cwd: agent.cwd,
|
||||||
};
|
};
|
||||||
@@ -701,7 +804,10 @@ export class AgentManager {
|
|||||||
throw new Error(`Agent ${agentId} not found`);
|
throw new Error(`Agent ${agentId} not found`);
|
||||||
}
|
}
|
||||||
const runtime = this.getRuntime(agent);
|
const runtime = this.getRuntime(agent);
|
||||||
return runtime?.availableModes ?? null;
|
return buildAvailableModes(
|
||||||
|
agent.options.type,
|
||||||
|
runtime?.availableModes ?? null
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -721,9 +827,12 @@ export class AgentManager {
|
|||||||
throw new Error(`Agent ${agentId} has no active session`);
|
throw new Error(`Agent ${agentId} has no active session`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate mode is available if we have the list
|
const availableModes = buildAvailableModes(
|
||||||
const availableModes = runtime.availableModes ?? [];
|
agent.options.type,
|
||||||
if (availableModes.length > 0) {
|
runtime.availableModes ?? null
|
||||||
|
);
|
||||||
|
|
||||||
|
if (availableModes && availableModes.length > 0) {
|
||||||
const mode = availableModes.find((m) => m.id === modeId);
|
const mode = availableModes.find((m) => m.id === modeId);
|
||||||
if (!mode) {
|
if (!mode) {
|
||||||
const availableIds = availableModes.map((m) => m.id).join(", ");
|
const availableIds = availableModes.map((m) => m.id).join(", ");
|
||||||
@@ -742,6 +851,7 @@ export class AgentManager {
|
|||||||
const updatedRuntime: AgentRuntime = {
|
const updatedRuntime: AgentRuntime = {
|
||||||
...runtime,
|
...runtime,
|
||||||
currentModeId: modeId,
|
currentModeId: modeId,
|
||||||
|
availableModes,
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (agent.state.type) {
|
switch (agent.state.type) {
|
||||||
@@ -832,11 +942,13 @@ export class AgentManager {
|
|||||||
// Create the init promise
|
// Create the init promise
|
||||||
const initPromise = (async () => {
|
const initPromise = (async () => {
|
||||||
try {
|
try {
|
||||||
|
const definition = getAgentTypeDefinition(agent.options.type);
|
||||||
const hasPersistedSession =
|
const hasPersistedSession =
|
||||||
(agent.options.type === "claude" &&
|
definition.supportsSessionPersistence &&
|
||||||
|
((agent.options.type === "claude" &&
|
||||||
agent.options.sessionId !== null) ||
|
agent.options.sessionId !== null) ||
|
||||||
!!persistedSessionId;
|
!!persistedSessionId);
|
||||||
const mode = hasPersistedSession ? "resume" : "new";
|
const mode: "new" | "resume" = hasPersistedSession ? "resume" : "new";
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`[Agent ${agentId}] Starting lazy initialization (mode: ${mode})`
|
`[Agent ${agentId}] Starting lazy initialization (mode: ${mode})`
|
||||||
@@ -965,7 +1077,8 @@ export class AgentManager {
|
|||||||
throw new Error(errorMessage);
|
throw new Error(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
const agentProcess = spawn("npx", ["@boudra/claude-code-acp"], {
|
const definition = getAgentTypeDefinition(agent.options.type);
|
||||||
|
const agentProcess = spawn(definition.spawn.command, definition.spawn.args, {
|
||||||
stdio: ["pipe", "pipe", "pipe"],
|
stdio: ["pipe", "pipe", "pipe"],
|
||||||
cwd,
|
cwd,
|
||||||
});
|
});
|
||||||
@@ -1040,11 +1153,15 @@ export class AgentManager {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const supportsResume = definition.supportsSessionPersistence;
|
||||||
|
const canResume = supportsResume && mode === "resume";
|
||||||
|
|
||||||
let sessionResponse:
|
let sessionResponse:
|
||||||
| Awaited<ReturnType<typeof connection.newSession>>
|
| Awaited<ReturnType<typeof connection.newSession>>
|
||||||
| Awaited<ReturnType<typeof connection.loadSession>>;
|
| Awaited<ReturnType<typeof connection.loadSession>>;
|
||||||
let effectiveSessionId: string;
|
let effectiveSessionId: string;
|
||||||
if (mode === "resume") {
|
|
||||||
|
if (canResume) {
|
||||||
const sessionIdToResume =
|
const sessionIdToResume =
|
||||||
(agent.options.type === "claude" && agent.options.sessionId) ||
|
(agent.options.type === "claude" && agent.options.sessionId) ||
|
||||||
resumeSessionId ||
|
resumeSessionId ||
|
||||||
@@ -1064,7 +1181,13 @@ export class AgentManager {
|
|||||||
});
|
});
|
||||||
effectiveSessionId = sessionIdToResume;
|
effectiveSessionId = sessionIdToResume;
|
||||||
} else {
|
} else {
|
||||||
console.log(`[Agent ${agentId}] Creating new session`);
|
if (mode === "resume" && !supportsResume) {
|
||||||
|
console.log(
|
||||||
|
`[Agent ${agentId}] Resume requested but unsupported for type '${definition.id}', starting new session`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log(`[Agent ${agentId}] Creating new session`);
|
||||||
|
}
|
||||||
const newSessionResponse = await connection.newSession({
|
const newSessionResponse = await connection.newSession({
|
||||||
cwd,
|
cwd,
|
||||||
mcpServers: [],
|
mcpServers: [],
|
||||||
@@ -1074,16 +1197,37 @@ export class AgentManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
runtime.sessionId = effectiveSessionId;
|
runtime.sessionId = effectiveSessionId;
|
||||||
runtime.currentModeId = sessionResponse.modes?.currentModeId ?? null;
|
|
||||||
runtime.availableModes = sessionResponse.modes?.availableModes ?? null;
|
|
||||||
|
|
||||||
const claudeSessionId = sessionResponse._meta?.claudeSessionId as
|
const modeSelection = resolveModeSelection({
|
||||||
| string
|
type: agent.options.type,
|
||||||
| undefined;
|
requestedModeId: sessionResponse.modes?.currentModeId ?? null,
|
||||||
|
runtimeModes: sessionResponse.modes?.availableModes ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
|
runtime.availableModes = modeSelection.availableModes;
|
||||||
|
runtime.currentModeId = modeSelection.modeId;
|
||||||
|
|
||||||
|
if (
|
||||||
|
sessionResponse.modes?.currentModeId &&
|
||||||
|
modeSelection.wasAdjusted
|
||||||
|
) {
|
||||||
|
console.warn(
|
||||||
|
`[Agent ${agentId}] Mode '${sessionResponse.modes.currentModeId}' not available for type '${agent.options.type}', using '${modeSelection.modeId ?? "none"}' instead.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const claudeSessionId =
|
||||||
|
sessionResponse._meta?.claudeSessionId !== undefined
|
||||||
|
? (sessionResponse._meta?.claudeSessionId as string | undefined)
|
||||||
|
: undefined;
|
||||||
console.log(
|
console.log(
|
||||||
`[Agent ${agentId}] Session ${
|
`[Agent ${agentId}] Session ${
|
||||||
mode === "new" ? "created" : "loaded"
|
canResume ? "loaded" : "created"
|
||||||
}: ACP=${effectiveSessionId}, Claude=${claudeSessionId || "N/A"}`
|
}: ACP=${effectiveSessionId}${
|
||||||
|
agent.options.type === "claude"
|
||||||
|
? `, Claude=${claudeSessionId || "N/A"}`
|
||||||
|
: ""
|
||||||
|
}`
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.updateAgent(agentId, (managedAgent) => {
|
await this.updateAgent(agentId, (managedAgent) => {
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ export const AgentOptionsSchema = z.discriminatedUnion("type", [
|
|||||||
type: z.literal("claude"),
|
type: z.literal("claude"),
|
||||||
sessionId: z.string().nullable(), // Claude's internal session ID (null until first prompt)
|
sessionId: z.string().nullable(), // Claude's internal session ID (null until first prompt)
|
||||||
}),
|
}),
|
||||||
// Add more agent types here in the future
|
z.object({
|
||||||
|
type: z.literal("codex"),
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type AgentOptions = z.infer<typeof AgentOptionsSchema>;
|
export type AgentOptions = z.infer<typeof AgentOptionsSchema>;
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
export type AgentType = "claude" | "codex";
|
||||||
|
|
||||||
|
export interface AgentModeDefinition {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AgentTypeDefinition {
|
||||||
|
id: AgentType;
|
||||||
|
label: string;
|
||||||
|
description?: string;
|
||||||
|
spawn: {
|
||||||
|
command: string;
|
||||||
|
args: string[];
|
||||||
|
};
|
||||||
|
supportsSessionPersistence: boolean;
|
||||||
|
availableModes: AgentModeDefinition[];
|
||||||
|
defaultModeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CLAUDE_MODES: AgentModeDefinition[] = [
|
||||||
|
{
|
||||||
|
id: "plan",
|
||||||
|
name: "Plan",
|
||||||
|
description: "Plan and design before implementing",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "bypassPermissions",
|
||||||
|
name: "Bypass Permissions",
|
||||||
|
description: "Skip permission prompts for faster execution",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const CODEX_MODES: AgentModeDefinition[] = [
|
||||||
|
{
|
||||||
|
id: "read-only",
|
||||||
|
name: "Read Only",
|
||||||
|
description: "Codex can read files and answer questions. Codex requires approval to make edits, run commands, or access network",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "auto",
|
||||||
|
name: "Auto",
|
||||||
|
description: "Codex can read files, make edits, and run commands in the workspace. Codex requires approval to work outside the workspace or access network",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "full-access",
|
||||||
|
name: "Full Access",
|
||||||
|
description: "Codex can read files, make edits, and run commands with network access, without approval. Exercise caution",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const agentTypeDefinitions: Record<AgentType, AgentTypeDefinition> = {
|
||||||
|
claude: {
|
||||||
|
id: "claude",
|
||||||
|
label: "Claude Code",
|
||||||
|
description: "Full Claude Code agent with session persistence",
|
||||||
|
spawn: {
|
||||||
|
command: "npx",
|
||||||
|
args: ["@boudra/claude-code-acp"],
|
||||||
|
},
|
||||||
|
supportsSessionPersistence: true,
|
||||||
|
availableModes: CLAUDE_MODES,
|
||||||
|
defaultModeId: "plan",
|
||||||
|
},
|
||||||
|
codex: {
|
||||||
|
id: "codex",
|
||||||
|
label: "Codex",
|
||||||
|
description: "Zed Codex ACP integration (no session resume)",
|
||||||
|
spawn: {
|
||||||
|
command: "npx",
|
||||||
|
args: ["@zed-industries/codex-acp"],
|
||||||
|
},
|
||||||
|
supportsSessionPersistence: false,
|
||||||
|
availableModes: CODEX_MODES,
|
||||||
|
defaultModeId: "auto",
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export function getAgentTypeDefinition(type: AgentType): AgentTypeDefinition {
|
||||||
|
return agentTypeDefinitions[type];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAgentModes(type: AgentType): AgentModeDefinition[] {
|
||||||
|
return agentTypeDefinitions[type].availableModes;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listAgentTypeDefinitions(): AgentTypeDefinition[] {
|
||||||
|
return Object.values(agentTypeDefinitions);
|
||||||
|
}
|
||||||
@@ -121,6 +121,7 @@ export async function createAgentMcpServer(
|
|||||||
|
|
||||||
const agentId = await agentManager.createAgent({
|
const agentId = await agentManager.createAgent({
|
||||||
cwd: resolvedCwd,
|
cwd: resolvedCwd,
|
||||||
|
type: "claude",
|
||||||
initialPrompt,
|
initialPrompt,
|
||||||
initialMode,
|
initialMode,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
#!/usr/bin/env tsx
|
|
||||||
|
|
||||||
import { AgentManager } from "./agent-manager.js";
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
console.log("=== ACP Agent Manager Test ===\n");
|
|
||||||
|
|
||||||
const manager = new AgentManager();
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log("1. Creating agent...");
|
|
||||||
const agentId = await manager.createAgent({
|
|
||||||
cwd: process.cwd(),
|
|
||||||
});
|
|
||||||
console.log(` ✓ Agent created: ${agentId}\n`);
|
|
||||||
|
|
||||||
console.log("2. Subscribing to updates...");
|
|
||||||
manager.subscribeToUpdates(agentId, (update) => {
|
|
||||||
console.log(` [UPDATE] ${new Date().toISOString()}`);
|
|
||||||
console.log(` Type: ${(update.notification as any).type}`);
|
|
||||||
console.log(` Notification:`, JSON.stringify(update.notification, null, 2));
|
|
||||||
console.log();
|
|
||||||
});
|
|
||||||
console.log(" ✓ Subscribed\n");
|
|
||||||
|
|
||||||
console.log("3. Checking initial status...");
|
|
||||||
const status = manager.getAgentStatus(agentId);
|
|
||||||
console.log(` Status: ${status}\n`);
|
|
||||||
|
|
||||||
console.log("4. Listing all agents...");
|
|
||||||
const agents = manager.listAgents();
|
|
||||||
console.log(` Total agents: ${agents.length}`);
|
|
||||||
agents.forEach((agent) => {
|
|
||||||
console.log(
|
|
||||||
` - ${agent.id}: ${agent.status} (created: ${agent.createdAt.toISOString()})`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
console.log();
|
|
||||||
|
|
||||||
console.log("5. Sending prompt: 'List files in current directory'");
|
|
||||||
await manager.sendPrompt(
|
|
||||||
agentId,
|
|
||||||
"List files in current directory using ls"
|
|
||||||
);
|
|
||||||
console.log(" ✓ Prompt sent\n");
|
|
||||||
|
|
||||||
console.log("6. Waiting for completion (30 seconds max)...");
|
|
||||||
const startTime = Date.now();
|
|
||||||
const maxWait = 30000; // 30 seconds
|
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
|
||||||
const checkInterval = setInterval(() => {
|
|
||||||
const currentStatus = manager.getAgentStatus(agentId);
|
|
||||||
const elapsed = Date.now() - startTime;
|
|
||||||
|
|
||||||
if (currentStatus === "completed") {
|
|
||||||
clearInterval(checkInterval);
|
|
||||||
console.log(` ✓ Agent completed in ${elapsed}ms\n`);
|
|
||||||
resolve();
|
|
||||||
} else if (currentStatus === "failed") {
|
|
||||||
clearInterval(checkInterval);
|
|
||||||
const agents = manager.listAgents();
|
|
||||||
const agent = agents.find((a) => a.id === agentId);
|
|
||||||
reject(new Error(`Agent failed: ${agent?.error || "Unknown error"}`));
|
|
||||||
} else if (elapsed > maxWait) {
|
|
||||||
clearInterval(checkInterval);
|
|
||||||
console.log(` ⚠ Timeout after ${elapsed}ms (status: ${currentStatus})\n`);
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("7. Cleaning up...");
|
|
||||||
await manager.killAgent(agentId);
|
|
||||||
console.log(" ✓ Agent killed\n");
|
|
||||||
|
|
||||||
console.log("=== Test completed successfully ===");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("\n❌ Test failed:");
|
|
||||||
console.error(error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((error) => {
|
|
||||||
console.error("Fatal error:", error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,610 +0,0 @@
|
|||||||
#!/usr/bin/env tsx
|
|
||||||
|
|
||||||
import { AgentManager } from "./agent-manager.js";
|
|
||||||
import { mkdtemp, rm, readFile, access } from "fs/promises";
|
|
||||||
import { tmpdir } from "os";
|
|
||||||
import { join } from "path";
|
|
||||||
import type { AgentUpdate, AgentStatus } from "./types.js";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Comprehensive Test Suite for ACP Agent Functionality
|
|
||||||
*
|
|
||||||
* This test validates all core features with a REAL Claude Code agent (no mocking):
|
|
||||||
*
|
|
||||||
* 1. Directory Control - Agent runs in specified directory
|
|
||||||
* 2. Permission Mode: Auto Approve - File operations are automatically approved
|
|
||||||
* 3. Permission Mode: Reject All - File operations are blocked
|
|
||||||
* 4. Multiple Prompts - Agent can handle sequential prompts
|
|
||||||
* 5. Update Streaming - All update types stream correctly (message chunks, tool calls, etc.)
|
|
||||||
* 6. State Management - Status accurately reflects actual agent state
|
|
||||||
*
|
|
||||||
* Run with: npx tsx src/server/acp/test-comprehensive.ts
|
|
||||||
*/
|
|
||||||
|
|
||||||
interface TestContext {
|
|
||||||
manager: AgentManager;
|
|
||||||
tmpDir: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CollectedUpdates {
|
|
||||||
all: AgentUpdate[];
|
|
||||||
messageChunks: string[];
|
|
||||||
toolCalls: any[];
|
|
||||||
toolResults: any[];
|
|
||||||
statusUpdates: any[];
|
|
||||||
commands: any[];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const manager = new AgentManager();
|
|
||||||
const tmpDir = await mkdtemp(join(tmpdir(), "acp-test-"));
|
|
||||||
|
|
||||||
console.log("=== Comprehensive ACP Agent Test Suite ===");
|
|
||||||
console.log(`Test directory: ${tmpDir}\n`);
|
|
||||||
|
|
||||||
const ctx: TestContext = { manager, tmpDir };
|
|
||||||
|
|
||||||
try {
|
|
||||||
await testDirectoryAndInitialPrompt(ctx);
|
|
||||||
await testPermissionAutoApprove(ctx);
|
|
||||||
await testPermissionRejectAll(ctx);
|
|
||||||
await testMultiplePrompts(ctx);
|
|
||||||
await testUpdateStreaming(ctx);
|
|
||||||
await testStateManagement(ctx);
|
|
||||||
|
|
||||||
console.log("\n✅ ALL TESTS PASSED!");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("\n❌ TEST SUITE FAILED:", error);
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
// Cleanup
|
|
||||||
console.log("\n=== Cleanup ===");
|
|
||||||
const agents = manager.listAgents();
|
|
||||||
console.log(`Killing ${agents.length} agents...`);
|
|
||||||
for (const agent of agents) {
|
|
||||||
await manager.killAgent(agent.id);
|
|
||||||
}
|
|
||||||
console.log(`Removing test directory: ${tmpDir}`);
|
|
||||||
await rm(tmpDir, { recursive: true, force: true });
|
|
||||||
console.log("Cleanup complete");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TEST 1: Directory Control and Initial Prompt
|
|
||||||
* Verifies that the agent runs in the specified directory
|
|
||||||
* and can execute the initial prompt successfully
|
|
||||||
*/
|
|
||||||
async function testDirectoryAndInitialPrompt(ctx: TestContext) {
|
|
||||||
console.log("\n=== TEST 1: Directory Control and Initial Prompt ===");
|
|
||||||
|
|
||||||
const updates = createUpdateCollector();
|
|
||||||
let unsubscribe: (() => void) | null = null;
|
|
||||||
|
|
||||||
// Create agent without initial prompt first, so we can subscribe
|
|
||||||
const agentId = await ctx.manager.createAgent({
|
|
||||||
cwd: ctx.tmpDir,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`✓ Agent created: ${agentId}`);
|
|
||||||
|
|
||||||
// Subscribe before sending prompt
|
|
||||||
unsubscribe = ctx.manager.subscribeToUpdates(agentId, (update) => {
|
|
||||||
collectUpdate(updates, update);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Now send the prompt
|
|
||||||
await ctx.manager.sendPrompt(
|
|
||||||
agentId,
|
|
||||||
"Run pwd and confirm you're in the test directory. Output the full path."
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Wait for processing to complete
|
|
||||||
await waitForStatus(ctx.manager, agentId, "completed", 60000);
|
|
||||||
|
|
||||||
console.log(`✓ Agent completed processing`);
|
|
||||||
console.log(` - Total updates: ${updates.all.length}`);
|
|
||||||
console.log(` - Message chunks: ${updates.messageChunks.length}`);
|
|
||||||
console.log(` - Tool calls: ${updates.toolCalls.length}`);
|
|
||||||
|
|
||||||
// Verify the output contains the tmpDir path
|
|
||||||
const fullMessage = updates.messageChunks.join("");
|
|
||||||
if (!fullMessage.includes(ctx.tmpDir)) {
|
|
||||||
throw new Error(
|
|
||||||
`Expected output to contain tmpDir path "${ctx.tmpDir}", but got: ${fullMessage}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`✓ Output contains correct directory path`);
|
|
||||||
|
|
||||||
// Verify we got tool calls (pwd command should be executed via terminal)
|
|
||||||
if (updates.toolCalls.length === 0) {
|
|
||||||
console.log(`⚠ Warning: No tool calls detected (expected pwd command)`);
|
|
||||||
} else {
|
|
||||||
console.log(`✓ Tool calls detected: ${updates.toolCalls.length}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify status transitions
|
|
||||||
const statuses = extractStatusTransitions(updates);
|
|
||||||
if (statuses.length > 0) {
|
|
||||||
console.log(`✓ Status transitions: ${statuses.join(" → ")}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("✅ TEST 1 PASSED\n");
|
|
||||||
} finally {
|
|
||||||
if (unsubscribe) unsubscribe();
|
|
||||||
await ctx.manager.killAgent(agentId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TEST 2: Permission Mode - Auto Approve
|
|
||||||
* Verifies that file operations are automatically approved
|
|
||||||
*/
|
|
||||||
async function testPermissionAutoApprove(ctx: TestContext) {
|
|
||||||
console.log("\n=== TEST 2: Permission Mode - Auto Approve ===");
|
|
||||||
|
|
||||||
const updates = createUpdateCollector();
|
|
||||||
const testFile = join(ctx.tmpDir, "test-auto-approve.txt");
|
|
||||||
|
|
||||||
const agentId = await ctx.manager.createAgent({
|
|
||||||
cwd: ctx.tmpDir,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`✓ Agent created: ${agentId}`);
|
|
||||||
|
|
||||||
const unsubscribe = ctx.manager.subscribeToUpdates(agentId, (update) => {
|
|
||||||
collectUpdate(updates, update);
|
|
||||||
});
|
|
||||||
|
|
||||||
await ctx.manager.sendPrompt(
|
|
||||||
agentId,
|
|
||||||
`Create a file called "test-auto-approve.txt" with the content "hello from auto approve". You can write the file however you prefer.`
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Wait for completion
|
|
||||||
await waitForStatus(ctx.manager, agentId, "completed", 60000);
|
|
||||||
|
|
||||||
console.log(`✓ Agent completed processing`);
|
|
||||||
console.log(` - Total updates: ${updates.all.length}`);
|
|
||||||
console.log(` - Tool calls: ${updates.toolCalls.length}`);
|
|
||||||
|
|
||||||
// Verify file was created
|
|
||||||
try {
|
|
||||||
const content = await readFile(testFile, "utf-8");
|
|
||||||
console.log(`✓ File created with content: "${content.trim()}"`);
|
|
||||||
|
|
||||||
if (!content.includes("hello from auto approve")) {
|
|
||||||
throw new Error(
|
|
||||||
`Expected file content to include "hello from auto approve", got: ${content}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`✓ File content is correct`);
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(`File was not created or could not be read: ${error}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("✅ TEST 2 PASSED\n");
|
|
||||||
} finally {
|
|
||||||
unsubscribe();
|
|
||||||
await ctx.manager.killAgent(agentId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TEST 3: Permission Mode - Reject All
|
|
||||||
* Verifies that file operations are blocked
|
|
||||||
*/
|
|
||||||
async function testPermissionRejectAll(ctx: TestContext) {
|
|
||||||
console.log("\n=== TEST 3: Permission Mode - Reject All ===");
|
|
||||||
|
|
||||||
const updates = createUpdateCollector();
|
|
||||||
const testFile = join(ctx.tmpDir, "test-blocked.txt");
|
|
||||||
|
|
||||||
const agentId = await ctx.manager.createAgent({
|
|
||||||
cwd: ctx.tmpDir,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`✓ Agent created: ${agentId}`);
|
|
||||||
|
|
||||||
const unsubscribe = ctx.manager.subscribeToUpdates(agentId, (update) => {
|
|
||||||
collectUpdate(updates, update);
|
|
||||||
});
|
|
||||||
|
|
||||||
await ctx.manager.sendPrompt(
|
|
||||||
agentId,
|
|
||||||
`Create a file called "test-blocked.txt" with the content "this should be blocked".`
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Wait for completion (agent should complete even if permission denied)
|
|
||||||
await waitForStatus(ctx.manager, agentId, "completed", 60000);
|
|
||||||
|
|
||||||
console.log(`✓ Agent completed processing`);
|
|
||||||
console.log(` - Total updates: ${updates.all.length}`);
|
|
||||||
|
|
||||||
// Verify file was NOT created
|
|
||||||
try {
|
|
||||||
await access(testFile);
|
|
||||||
throw new Error(
|
|
||||||
"File should not have been created with reject_all mode"
|
|
||||||
);
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error.code === "ENOENT") {
|
|
||||||
console.log(`✓ File was correctly blocked from creation`);
|
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that the response indicates permission denial
|
|
||||||
const fullMessage = updates.messageChunks.join("").toLowerCase();
|
|
||||||
const hasPermissionMessage =
|
|
||||||
fullMessage.includes("permission") ||
|
|
||||||
fullMessage.includes("denied") ||
|
|
||||||
fullMessage.includes("blocked") ||
|
|
||||||
fullMessage.includes("unable") ||
|
|
||||||
fullMessage.includes("could not") ||
|
|
||||||
fullMessage.includes("cannot");
|
|
||||||
|
|
||||||
if (hasPermissionMessage) {
|
|
||||||
console.log(`✓ Agent indicated permission denial in response`);
|
|
||||||
} else {
|
|
||||||
console.log(
|
|
||||||
`⚠ Warning: Agent response doesn't clearly indicate permission denial`
|
|
||||||
);
|
|
||||||
console.log(` Response: ${fullMessage.substring(0, 200)}...`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("✅ TEST 3 PASSED\n");
|
|
||||||
} finally {
|
|
||||||
unsubscribe();
|
|
||||||
await ctx.manager.killAgent(agentId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TEST 4: Multiple Prompts
|
|
||||||
* Verifies that an agent can handle multiple sequential prompts
|
|
||||||
*/
|
|
||||||
async function testMultiplePrompts(ctx: TestContext) {
|
|
||||||
console.log("\n=== TEST 4: Multiple Prompts ===");
|
|
||||||
|
|
||||||
const updates1 = createUpdateCollector();
|
|
||||||
const updates2 = createUpdateCollector();
|
|
||||||
let collectingFirst = true;
|
|
||||||
|
|
||||||
const agentId = await ctx.manager.createAgent({
|
|
||||||
cwd: ctx.tmpDir,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`✓ Agent created: ${agentId}`);
|
|
||||||
|
|
||||||
const unsubscribe = ctx.manager.subscribeToUpdates(agentId, (update) => {
|
|
||||||
if (collectingFirst) {
|
|
||||||
collectUpdate(updates1, update);
|
|
||||||
} else {
|
|
||||||
collectUpdate(updates2, update);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Send first prompt
|
|
||||||
console.log("Sending prompt 1...");
|
|
||||||
await ctx.manager.sendPrompt(agentId, "Echo 'first prompt response'");
|
|
||||||
|
|
||||||
await waitForStatus(ctx.manager, agentId, "completed", 30000);
|
|
||||||
console.log(`✓ First prompt completed`);
|
|
||||||
console.log(` - Updates: ${updates1.all.length}`);
|
|
||||||
console.log(` - Message: ${updates1.messageChunks.join("").substring(0, 100)}...`);
|
|
||||||
|
|
||||||
// Switch to collecting second set of updates
|
|
||||||
collectingFirst = false;
|
|
||||||
|
|
||||||
// Send second prompt
|
|
||||||
console.log("Sending prompt 2...");
|
|
||||||
await ctx.manager.sendPrompt(agentId, "Echo 'second prompt response'");
|
|
||||||
|
|
||||||
await waitForStatus(ctx.manager, agentId, "completed", 30000);
|
|
||||||
console.log(`✓ Second prompt completed`);
|
|
||||||
console.log(` - Updates: ${updates2.all.length}`);
|
|
||||||
console.log(` - Message: ${updates2.messageChunks.join("").substring(0, 100)}...`);
|
|
||||||
|
|
||||||
// Verify both prompts got responses
|
|
||||||
if (updates1.all.length === 0 || updates2.all.length === 0) {
|
|
||||||
throw new Error("Both prompts should generate updates");
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`✓ Both prompts executed successfully`);
|
|
||||||
console.log("✅ TEST 4 PASSED\n");
|
|
||||||
} finally {
|
|
||||||
unsubscribe();
|
|
||||||
await ctx.manager.killAgent(agentId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TEST 5: Update Streaming
|
|
||||||
* Verifies that all update types are received correctly
|
|
||||||
*/
|
|
||||||
async function testUpdateStreaming(ctx: TestContext) {
|
|
||||||
console.log("\n=== TEST 5: Update Streaming ===");
|
|
||||||
|
|
||||||
const updates = createUpdateCollector();
|
|
||||||
|
|
||||||
const agentId = await ctx.manager.createAgent({
|
|
||||||
cwd: ctx.tmpDir,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`✓ Agent created: ${agentId}`);
|
|
||||||
|
|
||||||
const unsubscribe = ctx.manager.subscribeToUpdates(agentId, (update) => {
|
|
||||||
collectUpdate(updates, update);
|
|
||||||
});
|
|
||||||
|
|
||||||
await ctx.manager.sendPrompt(
|
|
||||||
agentId,
|
|
||||||
"List the files in the current directory using the Bash tool. Then explain what you found."
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await waitForStatus(ctx.manager, agentId, "completed", 60000);
|
|
||||||
|
|
||||||
console.log(`✓ Agent completed processing`);
|
|
||||||
console.log(`\nUpdate Statistics:`);
|
|
||||||
console.log(` - Total updates: ${updates.all.length}`);
|
|
||||||
console.log(` - Message chunks: ${updates.messageChunks.length}`);
|
|
||||||
console.log(` - Tool calls: ${updates.toolCalls.length}`);
|
|
||||||
console.log(` - Tool results: ${updates.toolResults.length}`);
|
|
||||||
console.log(` - Status updates: ${updates.statusUpdates.length}`);
|
|
||||||
console.log(` - Commands: ${updates.commands.length}`);
|
|
||||||
|
|
||||||
// Verify message can be reconstructed
|
|
||||||
const fullMessage = updates.messageChunks.join("");
|
|
||||||
console.log(`\nReconstructed message length: ${fullMessage.length} chars`);
|
|
||||||
console.log(`First 150 chars: ${fullMessage.substring(0, 150)}...`);
|
|
||||||
|
|
||||||
if (fullMessage.length === 0) {
|
|
||||||
throw new Error("Expected to receive message chunks");
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`✓ Message chunks successfully reconstructed`);
|
|
||||||
|
|
||||||
// Verify we got tool calls
|
|
||||||
if (updates.toolCalls.length === 0) {
|
|
||||||
throw new Error("Expected at least one tool call");
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`✓ Tool calls captured: ${updates.toolCalls.length}`);
|
|
||||||
|
|
||||||
// Log tool call details
|
|
||||||
updates.toolCalls.forEach((call, i) => {
|
|
||||||
console.log(` Tool ${i + 1}: ${JSON.stringify(call).substring(0, 100)}...`);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("✅ TEST 5 PASSED\n");
|
|
||||||
} finally {
|
|
||||||
unsubscribe();
|
|
||||||
await ctx.manager.killAgent(agentId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TEST 6: State Management
|
|
||||||
* Verifies that agent status accurately reflects actual state
|
|
||||||
*/
|
|
||||||
async function testStateManagement(ctx: TestContext) {
|
|
||||||
console.log("\n=== TEST 6: State Management ===");
|
|
||||||
|
|
||||||
const statusLog: Array<{ time: Date; status: AgentStatus }> = [];
|
|
||||||
|
|
||||||
const agentId = await ctx.manager.createAgent({
|
|
||||||
cwd: ctx.tmpDir,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`✓ Agent created: ${agentId}`);
|
|
||||||
|
|
||||||
await ctx.manager.sendPrompt(
|
|
||||||
agentId,
|
|
||||||
"Sleep for 2 seconds using a bash command, then echo 'done'"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Track status changes
|
|
||||||
const checkInterval = setInterval(() => {
|
|
||||||
const status = ctx.manager.getAgentStatus(agentId);
|
|
||||||
statusLog.push({ time: new Date(), status });
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Wait for completion
|
|
||||||
await waitForStatus(ctx.manager, agentId, "completed", 60000);
|
|
||||||
|
|
||||||
clearInterval(checkInterval);
|
|
||||||
|
|
||||||
console.log(`✓ Agent completed processing`);
|
|
||||||
console.log(`\nStatus transitions logged: ${statusLog.length}`);
|
|
||||||
|
|
||||||
// Analyze status transitions
|
|
||||||
const transitions: string[] = [];
|
|
||||||
let lastStatus: AgentStatus | null = null;
|
|
||||||
|
|
||||||
for (const entry of statusLog) {
|
|
||||||
if (entry.status !== lastStatus) {
|
|
||||||
transitions.push(entry.status);
|
|
||||||
lastStatus = entry.status;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Status transition sequence: ${transitions.join(" → ")}`);
|
|
||||||
|
|
||||||
// Verify expected transitions occurred
|
|
||||||
const expectedStates = ["ready", "processing", "completed"];
|
|
||||||
for (const expectedState of expectedStates) {
|
|
||||||
if (!transitions.includes(expectedState as AgentStatus)) {
|
|
||||||
console.log(
|
|
||||||
`⚠ Warning: Expected state "${expectedState}" not observed`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.log(`✓ State "${expectedState}" observed`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify final status is completed
|
|
||||||
const finalStatus = ctx.manager.getAgentStatus(agentId);
|
|
||||||
if (finalStatus !== "completed") {
|
|
||||||
throw new Error(
|
|
||||||
`Expected final status to be "completed", got "${finalStatus}"`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`✓ Final status is correct: ${finalStatus}`);
|
|
||||||
|
|
||||||
// Kill the agent and verify status changes
|
|
||||||
await ctx.manager.killAgent(agentId);
|
|
||||||
|
|
||||||
// Agent should be removed from manager
|
|
||||||
try {
|
|
||||||
ctx.manager.getAgentStatus(agentId);
|
|
||||||
throw new Error("Agent should be removed after killing");
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error.message.includes("not found")) {
|
|
||||||
console.log(`✓ Agent correctly removed after kill`);
|
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("✅ TEST 6 PASSED\n");
|
|
||||||
} finally {
|
|
||||||
clearInterval(checkInterval);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper: Create an update collector
|
|
||||||
*/
|
|
||||||
function createUpdateCollector(): CollectedUpdates {
|
|
||||||
return {
|
|
||||||
all: [],
|
|
||||||
messageChunks: [],
|
|
||||||
toolCalls: [],
|
|
||||||
toolResults: [],
|
|
||||||
statusUpdates: [],
|
|
||||||
commands: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper: Collect and categorize an update
|
|
||||||
*/
|
|
||||||
function collectUpdate(collector: CollectedUpdates, update: AgentUpdate): void {
|
|
||||||
collector.all.push(update);
|
|
||||||
|
|
||||||
const notification = update.notification as any;
|
|
||||||
|
|
||||||
// The notification structure can be:
|
|
||||||
// 1. { type: "sessionUpdate", sessionUpdate: { ... } } - for status updates
|
|
||||||
// 2. { sessionUpdate: "agent_message_chunk", content: { ... } } - for message chunks
|
|
||||||
// 3. { sessionUpdate: "tool_call", ... } - for tool calls
|
|
||||||
// 4. { availableCommands: [...] } - for available commands
|
|
||||||
|
|
||||||
// Message chunks
|
|
||||||
if (notification.sessionUpdate === "agent_message_chunk" && notification.content) {
|
|
||||||
const content = notification.content;
|
|
||||||
if (content.type === "text" && content.text) {
|
|
||||||
collector.messageChunks.push(content.text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tool calls
|
|
||||||
if (notification.sessionUpdate === "tool_call") {
|
|
||||||
collector.toolCalls.push(notification);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tool results
|
|
||||||
if (notification.sessionUpdate === "tool_result") {
|
|
||||||
collector.toolResults.push(notification);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Status updates from the wrapper
|
|
||||||
if (notification.type === "sessionUpdate" && notification.sessionUpdate?.status) {
|
|
||||||
collector.statusUpdates.push(notification.sessionUpdate.status);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Status updates from our custom type
|
|
||||||
if (notification.sessionUpdate === "status_change") {
|
|
||||||
collector.statusUpdates.push(notification.status);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Available commands
|
|
||||||
if (notification.availableCommands) {
|
|
||||||
collector.commands.push(notification.availableCommands);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper: Extract status transitions from updates
|
|
||||||
*/
|
|
||||||
function extractStatusTransitions(collector: CollectedUpdates): string[] {
|
|
||||||
const statuses: string[] = [];
|
|
||||||
let lastStatus: string | null = null;
|
|
||||||
|
|
||||||
for (const statusUpdate of collector.statusUpdates) {
|
|
||||||
if (statusUpdate !== lastStatus) {
|
|
||||||
statuses.push(String(statusUpdate));
|
|
||||||
lastStatus = statusUpdate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return statuses;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper: Wait for agent to reach a specific status
|
|
||||||
*/
|
|
||||||
async function waitForStatus(
|
|
||||||
manager: AgentManager,
|
|
||||||
agentId: string,
|
|
||||||
targetStatus: AgentStatus,
|
|
||||||
timeoutMs: number
|
|
||||||
): Promise<void> {
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
while (Date.now() - startTime < timeoutMs) {
|
|
||||||
const currentStatus = manager.getAgentStatus(agentId);
|
|
||||||
|
|
||||||
if (currentStatus === targetStatus) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentStatus === "failed") {
|
|
||||||
const agent = manager.listAgents().find((a) => a.id === agentId);
|
|
||||||
throw new Error(
|
|
||||||
`Agent failed while waiting for status "${targetStatus}": ${agent?.error}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait a bit before checking again
|
|
||||||
await sleep(500);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(
|
|
||||||
`Timeout waiting for status "${targetStatus}" (current: ${manager.getAgentStatus(agentId)})`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper: Sleep for specified milliseconds
|
|
||||||
*/
|
|
||||||
function sleep(ms: number): Promise<void> {
|
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the test suite
|
|
||||||
main().catch((error) => {
|
|
||||||
console.error("Fatal error:", error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
#!/usr/bin/env tsx
|
|
||||||
|
|
||||||
import { AgentManager } from "./agent-manager.js";
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
console.log("=== ACP Concurrent Agents Test ===\n");
|
|
||||||
|
|
||||||
const manager = new AgentManager();
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log("1. Creating multiple agents...");
|
|
||||||
const agent1Id = await manager.createAgent({
|
|
||||||
cwd: process.cwd(),
|
|
||||||
});
|
|
||||||
const agent2Id = await manager.createAgent({
|
|
||||||
cwd: process.cwd(),
|
|
||||||
});
|
|
||||||
console.log(` ✓ Agent 1: ${agent1Id}`);
|
|
||||||
console.log(` ✓ Agent 2: ${agent2Id}\n`);
|
|
||||||
|
|
||||||
console.log("2. Setting up update handlers...");
|
|
||||||
let agent1Updates = 0;
|
|
||||||
let agent2Updates = 0;
|
|
||||||
|
|
||||||
manager.subscribeToUpdates(agent1Id, () => {
|
|
||||||
agent1Updates++;
|
|
||||||
});
|
|
||||||
|
|
||||||
manager.subscribeToUpdates(agent2Id, () => {
|
|
||||||
agent2Updates++;
|
|
||||||
});
|
|
||||||
console.log(" ✓ Handlers set up\n");
|
|
||||||
|
|
||||||
console.log("3. Sending prompts to both agents...");
|
|
||||||
await manager.sendPrompt(agent1Id, "Echo 'Agent 1 says hello'");
|
|
||||||
await manager.sendPrompt(agent2Id, "Echo 'Agent 2 says hello'");
|
|
||||||
console.log(" ✓ Prompts sent\n");
|
|
||||||
|
|
||||||
console.log("4. Waiting for activity (5 seconds)...");
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
||||||
console.log(` Agent 1 updates: ${agent1Updates}`);
|
|
||||||
console.log(` Agent 2 updates: ${agent2Updates}\n`);
|
|
||||||
|
|
||||||
console.log("5. Listing all agents...");
|
|
||||||
const agents = manager.listAgents();
|
|
||||||
console.log(` Total agents: ${agents.length}`);
|
|
||||||
agents.forEach((agent) => {
|
|
||||||
console.log(` - ${agent.id.slice(0, 8)}: ${agent.status}`);
|
|
||||||
});
|
|
||||||
console.log();
|
|
||||||
|
|
||||||
console.log("6. Testing cancellation on agent 1...");
|
|
||||||
const status1Before = manager.getAgentStatus(agent1Id);
|
|
||||||
console.log(` Agent 1 status before cancel: ${status1Before}`);
|
|
||||||
|
|
||||||
await manager.cancelAgent(agent1Id);
|
|
||||||
const status1After = manager.getAgentStatus(agent1Id);
|
|
||||||
console.log(` Agent 1 status after cancel: ${status1After}\n`);
|
|
||||||
|
|
||||||
console.log("7. Cleaning up...");
|
|
||||||
await manager.killAgent(agent1Id);
|
|
||||||
await manager.killAgent(agent2Id);
|
|
||||||
console.log(" ✓ All agents killed\n");
|
|
||||||
|
|
||||||
console.log("8. Verifying cleanup...");
|
|
||||||
const remainingAgents = manager.listAgents();
|
|
||||||
console.log(` Remaining agents: ${remainingAgents.length}\n`);
|
|
||||||
|
|
||||||
console.log("=== Concurrent test completed successfully ===");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("\n❌ Test failed:");
|
|
||||||
console.error(error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((error) => {
|
|
||||||
console.error("Fatal error:", error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
#!/usr/bin/env tsx
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Integration test for Agent MCP Server with Session
|
|
||||||
*
|
|
||||||
* This test simulates a Session-like scenario where:
|
|
||||||
* 1. AgentManager is created
|
|
||||||
* 2. Agent MCP server is set up
|
|
||||||
* 3. Tools are called via the MCP client
|
|
||||||
* 4. Agent updates are streamed to a mock WebSocket handler
|
|
||||||
* 5. Cleanup is performed
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { AgentManager } from "./agent-manager.js";
|
|
||||||
import { createAgentMcpServer } from "./mcp-server.js";
|
|
||||||
import { experimental_createMCPClient } from "ai";
|
|
||||||
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
||||||
import type { AgentUpdate } from "./types.js";
|
|
||||||
|
|
||||||
async function sleep(ms: number): Promise<void> {
|
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
console.log("=== Agent MCP Integration Test ===\n");
|
|
||||||
|
|
||||||
// Step 1: Create AgentManager
|
|
||||||
console.log("1. Creating AgentManager...");
|
|
||||||
const agentManager = new AgentManager();
|
|
||||||
console.log(" ✓ AgentManager created\n");
|
|
||||||
|
|
||||||
// Step 2: Create Agent MCP Server
|
|
||||||
console.log("2. Creating Agent MCP Server...");
|
|
||||||
const server = await createAgentMcpServer({ agentManager });
|
|
||||||
console.log(" ✓ Agent MCP Server created\n");
|
|
||||||
|
|
||||||
// Step 3: Set up transport and client
|
|
||||||
console.log("3. Setting up MCP transport and client...");
|
|
||||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
||||||
await server.connect(serverTransport);
|
|
||||||
const mcpClient = await experimental_createMCPClient({
|
|
||||||
transport: clientTransport,
|
|
||||||
});
|
|
||||||
console.log(" ✓ MCP client connected\n");
|
|
||||||
|
|
||||||
// Step 4: Get tools from client
|
|
||||||
console.log("4. Getting tools from MCP client...");
|
|
||||||
const tools = await mcpClient.tools();
|
|
||||||
const toolNames = Object.keys(tools);
|
|
||||||
console.log(` ✓ Got ${toolNames.length} tools: ${toolNames.join(", ")}\n`);
|
|
||||||
|
|
||||||
// Step 5: Verify all expected tools are present
|
|
||||||
console.log("5. Verifying expected tools...");
|
|
||||||
const expectedTools = [
|
|
||||||
"create_coding_agent",
|
|
||||||
"send_agent_prompt",
|
|
||||||
"get_agent_status",
|
|
||||||
"list_agents",
|
|
||||||
"cancel_agent",
|
|
||||||
"kill_agent",
|
|
||||||
];
|
|
||||||
|
|
||||||
const missingTools = expectedTools.filter((t) => !toolNames.includes(t));
|
|
||||||
if (missingTools.length > 0) {
|
|
||||||
console.error(` ✗ Missing tools: ${missingTools.join(", ")}`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
console.log(" ✓ All expected tools present\n");
|
|
||||||
|
|
||||||
// Step 6: Test direct AgentManager usage (bypassing MCP tools for simplicity)
|
|
||||||
console.log("6. Creating agent directly via AgentManager...");
|
|
||||||
const receivedUpdates: AgentUpdate[] = [];
|
|
||||||
|
|
||||||
const agentId = await agentManager.createAgent({
|
|
||||||
initialPrompt: "Create a simple hello world function in TypeScript",
|
|
||||||
cwd: process.cwd(),
|
|
||||||
});
|
|
||||||
console.log(` ✓ Agent created with ID: ${agentId}\n`);
|
|
||||||
|
|
||||||
// Step 7: Subscribe to agent updates (simulating Session behavior)
|
|
||||||
console.log("7. Subscribing to agent updates...");
|
|
||||||
const unsubscribe = agentManager.subscribeToUpdates(agentId, (update) => {
|
|
||||||
const updateType = (update.notification as any).type || "unknown";
|
|
||||||
console.log(` 📡 Received update for agent ${update.agentId}: ${updateType}`);
|
|
||||||
receivedUpdates.push(update);
|
|
||||||
});
|
|
||||||
console.log(" ✓ Subscribed to agent updates\n");
|
|
||||||
|
|
||||||
// Step 8: Wait a bit for agent initialization
|
|
||||||
console.log("8. Waiting for agent to initialize...");
|
|
||||||
await sleep(2000);
|
|
||||||
console.log(" ✓ Wait complete\n");
|
|
||||||
|
|
||||||
// Step 9: Test list_agents via manager
|
|
||||||
console.log("9. Listing agents via AgentManager...");
|
|
||||||
const agents = agentManager.listAgents();
|
|
||||||
console.log(` ✓ Found ${agents.length} agent(s)`);
|
|
||||||
console.log(` Agents:`, agents.map(a => ({ id: a.id, status: a.status })));
|
|
||||||
console.log();
|
|
||||||
|
|
||||||
// Step 10: Test get_agent_status
|
|
||||||
console.log("10. Getting agent status...");
|
|
||||||
const status = agentManager.getAgentStatus(agentId);
|
|
||||||
console.log(` ✓ Agent status: ${status}\n`);
|
|
||||||
|
|
||||||
// Step 11: Test send_agent_prompt
|
|
||||||
console.log("11. Sending prompt to agent...");
|
|
||||||
try {
|
|
||||||
await agentManager.sendPrompt(agentId, "Show me what you created");
|
|
||||||
console.log(" ✓ Prompt sent successfully\n");
|
|
||||||
} catch (error) {
|
|
||||||
console.log(` ⚠ Prompt send failed (expected if agent not ready): ${error instanceof Error ? error.message : String(error)}\n`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 12: Wait a bit for any agent processing
|
|
||||||
console.log("12. Waiting for agent processing...");
|
|
||||||
await sleep(3000);
|
|
||||||
console.log(" ✓ Wait complete\n");
|
|
||||||
|
|
||||||
// Step 13: Test cancel_agent
|
|
||||||
console.log("13. Cancelling agent...");
|
|
||||||
try {
|
|
||||||
await agentManager.cancelAgent(agentId);
|
|
||||||
console.log(" ✓ Agent cancelled successfully\n");
|
|
||||||
} catch (error) {
|
|
||||||
console.log(` ⚠ Cancel failed (expected if agent not processing): ${error instanceof Error ? error.message : String(error)}\n`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 14: Unsubscribe from updates
|
|
||||||
console.log("14. Unsubscribing from agent updates...");
|
|
||||||
unsubscribe();
|
|
||||||
console.log(" ✓ Unsubscribed\n");
|
|
||||||
|
|
||||||
// Step 15: Test kill_agent
|
|
||||||
console.log("15. Killing agent...");
|
|
||||||
await agentManager.killAgent(agentId);
|
|
||||||
console.log(" ✓ Agent killed successfully\n");
|
|
||||||
|
|
||||||
// Step 16: Verify agent is gone
|
|
||||||
console.log("16. Verifying agent is removed...");
|
|
||||||
await sleep(1000);
|
|
||||||
const finalAgents = agentManager.listAgents();
|
|
||||||
if (finalAgents.some((a) => a.id === agentId)) {
|
|
||||||
console.error(" ✗ Agent still exists after kill");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
console.log(" ✓ Agent successfully removed\n");
|
|
||||||
|
|
||||||
// Step 17: Close MCP client
|
|
||||||
console.log("17. Closing MCP client...");
|
|
||||||
await mcpClient.close();
|
|
||||||
console.log(" ✓ MCP client closed\n");
|
|
||||||
|
|
||||||
// Summary
|
|
||||||
console.log("=== Test Summary ===");
|
|
||||||
console.log(`✓ All tests passed`);
|
|
||||||
console.log(`✓ Received ${receivedUpdates.length} agent updates during test`);
|
|
||||||
console.log("\nIntegration test completed successfully!");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the test
|
|
||||||
main().catch((error) => {
|
|
||||||
console.error("\n❌ Test failed with error:");
|
|
||||||
console.error(error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
#!/usr/bin/env tsx
|
|
||||||
|
|
||||||
import { AgentManager } from "./agent-manager.js";
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
console.log("=== ACP Simple Test ===\n");
|
|
||||||
|
|
||||||
const manager = new AgentManager();
|
|
||||||
let updateCount = 0;
|
|
||||||
let toolCallCount = 0;
|
|
||||||
let messageChunks: string[] = [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log("1. Creating agent...");
|
|
||||||
const agentId = await manager.createAgent({
|
|
||||||
cwd: process.cwd(),
|
|
||||||
});
|
|
||||||
console.log(` ✓ Created: ${agentId}\n`);
|
|
||||||
|
|
||||||
console.log("2. Setting up minimal update handler...");
|
|
||||||
manager.subscribeToUpdates(agentId, (update) => {
|
|
||||||
updateCount++;
|
|
||||||
const sessionUpdate = (update.notification as any).sessionUpdate;
|
|
||||||
|
|
||||||
if (sessionUpdate === "tool_call") {
|
|
||||||
toolCallCount++;
|
|
||||||
console.log(` [Tool Call] ${(update.notification as any).title}`);
|
|
||||||
} else if (sessionUpdate === "agent_message_chunk") {
|
|
||||||
const text = (update.notification as any).content?.text || "";
|
|
||||||
if (text.trim()) {
|
|
||||||
messageChunks.push(text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
console.log(" ✓ Handler set up\n");
|
|
||||||
|
|
||||||
console.log("3. Sending prompt...");
|
|
||||||
await manager.sendPrompt(agentId, "List 3 files in current directory");
|
|
||||||
console.log(" ✓ Prompt sent\n");
|
|
||||||
|
|
||||||
console.log("4. Waiting for completion (10 seconds)...");
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 10000));
|
|
||||||
|
|
||||||
console.log("\n=== Results ===");
|
|
||||||
console.log(`Total updates: ${updateCount}`);
|
|
||||||
console.log(`Tool calls: ${toolCallCount}`);
|
|
||||||
console.log(`Message length: ${messageChunks.join("").length} chars`);
|
|
||||||
console.log(`First few chunks: ${messageChunks.slice(0, 5).join("")}\n`);
|
|
||||||
|
|
||||||
console.log("5. Cleaning up...");
|
|
||||||
await manager.killAgent(agentId);
|
|
||||||
console.log(" ✓ Agent killed\n");
|
|
||||||
|
|
||||||
console.log("=== Simple test completed ===");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("\n❌ Test failed:");
|
|
||||||
console.error(error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch((error) => {
|
|
||||||
console.error("Fatal error:", error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -4,6 +4,7 @@ import type {
|
|||||||
ClientSideConnection,
|
ClientSideConnection,
|
||||||
} from "@agentclientprotocol/sdk";
|
} from "@agentclientprotocol/sdk";
|
||||||
import type { ChildProcess } from "child_process";
|
import type { ChildProcess } from "child_process";
|
||||||
|
import type { AgentModeDefinition, AgentType } from "./agent-types.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extended update types with messageId for proper deduplication
|
* Extended update types with messageId for proper deduplication
|
||||||
@@ -68,13 +69,10 @@ export type AgentStatus =
|
|||||||
| "killed";
|
| "killed";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Session mode definition from ACP
|
* Session mode definition from ACP.
|
||||||
|
* Alias to the shared agent mode definition used across the app.
|
||||||
*/
|
*/
|
||||||
export interface SessionMode {
|
export type SessionMode = AgentModeDefinition;
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runtime state for an initialized agent
|
* Runtime state for an initialized agent
|
||||||
@@ -117,7 +115,7 @@ export interface AgentInfo {
|
|||||||
status: AgentStatus;
|
status: AgentStatus;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
lastActivityAt: Date;
|
lastActivityAt: Date;
|
||||||
type: "claude";
|
type: AgentType;
|
||||||
sessionId: string | null;
|
sessionId: string | null;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
currentModeId: string | null;
|
currentModeId: string | null;
|
||||||
@@ -141,6 +139,7 @@ export interface AgentUpdate {
|
|||||||
*/
|
*/
|
||||||
export interface CreateAgentOptions {
|
export interface CreateAgentOptions {
|
||||||
cwd: string;
|
cwd: string;
|
||||||
|
type: AgentType;
|
||||||
initialPrompt?: string;
|
initialPrompt?: string;
|
||||||
initialMode?: string;
|
initialMode?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import type { AgentType } from "./acp/agent-types.js";
|
||||||
|
import { listAgentTypeDefinitions } from "./acp/agent-types.js";
|
||||||
|
|
||||||
const AgentModeSchema = z.object({
|
const AgentModeSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
@@ -6,12 +8,14 @@ const AgentModeSchema = z.object({
|
|||||||
description: z.string().nullable().optional(),
|
description: z.string().nullable().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const agentTypes = listAgentTypeDefinitions().map((definition) => definition.id);
|
||||||
|
|
||||||
const AgentInfoSchema = z.object({
|
const AgentInfoSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
status: z.string(),
|
status: z.string(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
lastActivityAt: z.date(),
|
lastActivityAt: z.date(),
|
||||||
type: z.literal("claude"),
|
type: z.enum(agentTypes as [AgentType, ...AgentType[]]),
|
||||||
sessionId: z.string().nullable(),
|
sessionId: z.string().nullable(),
|
||||||
error: z.string().nullable(),
|
error: z.string().nullable(),
|
||||||
currentModeId: z.string().nullable(),
|
currentModeId: z.string().nullable(),
|
||||||
@@ -95,6 +99,7 @@ export const CreateAgentRequestMessageSchema = z.object({
|
|||||||
cwd: z.string(),
|
cwd: z.string(),
|
||||||
initialMode: z.string().optional(),
|
initialMode: z.string().optional(),
|
||||||
worktreeName: z.string().optional(),
|
worktreeName: z.string().optional(),
|
||||||
|
agentType: z.enum(agentTypes as [AgentType, ...AgentType[]]).optional(),
|
||||||
requestId: z.string().optional(),
|
requestId: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -220,7 +225,7 @@ export const AgentCreatedMessageSchema = z.object({
|
|||||||
payload: z.object({
|
payload: z.object({
|
||||||
agentId: z.string(),
|
agentId: z.string(),
|
||||||
status: z.string(),
|
status: z.string(),
|
||||||
type: z.literal("claude"),
|
type: z.enum(agentTypes as [AgentType, ...AgentType[]]),
|
||||||
currentModeId: z.string().optional(),
|
currentModeId: z.string().optional(),
|
||||||
availableModes: z.array(AgentModeSchema).optional(),
|
availableModes: z.array(AgentModeSchema).optional(),
|
||||||
title: z.string().optional(),
|
title: z.string().optional(),
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ 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 type { AgentType } from "./acp/agent-types.js";
|
||||||
import {
|
import {
|
||||||
generateAgentTitle,
|
generateAgentTitle,
|
||||||
isTitleGeneratorInitialized,
|
isTitleGeneratorInitialized,
|
||||||
@@ -493,12 +494,13 @@ export class Session {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case "create_agent_request":
|
case "create_agent_request":
|
||||||
await this.handleCreateAgentRequest(
|
await this.handleCreateAgentRequest({
|
||||||
msg.cwd,
|
cwd: msg.cwd,
|
||||||
msg.initialMode,
|
initialMode: msg.initialMode,
|
||||||
msg.requestId,
|
requestId: msg.requestId,
|
||||||
msg.worktreeName
|
worktreeName: msg.worktreeName,
|
||||||
);
|
agentType: msg.agentType,
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "initialize_agent_request":
|
case "initialize_agent_request":
|
||||||
@@ -820,12 +822,19 @@ export class Session {
|
|||||||
/**
|
/**
|
||||||
* Handle create agent request
|
* Handle create agent request
|
||||||
*/
|
*/
|
||||||
private async handleCreateAgentRequest(
|
private async handleCreateAgentRequest({
|
||||||
cwd: string,
|
cwd,
|
||||||
initialMode?: string,
|
initialMode,
|
||||||
requestId?: string,
|
requestId,
|
||||||
worktreeName?: string
|
worktreeName,
|
||||||
): Promise<void> {
|
agentType,
|
||||||
|
}: {
|
||||||
|
cwd: string;
|
||||||
|
initialMode?: string;
|
||||||
|
requestId?: string;
|
||||||
|
worktreeName?: string;
|
||||||
|
agentType?: AgentType;
|
||||||
|
}): Promise<void> {
|
||||||
console.log(
|
console.log(
|
||||||
`[Session ${this.clientId}] Creating agent in ${cwd} with mode ${
|
`[Session ${this.clientId}] Creating agent in ${cwd} with mode ${
|
||||||
initialMode || "default"
|
initialMode || "default"
|
||||||
@@ -859,8 +868,11 @@ export class Session {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizedType: AgentType = agentType ?? "claude";
|
||||||
|
|
||||||
const agentId = await this.agentManager.createAgent({
|
const agentId = await this.agentManager.createAgent({
|
||||||
cwd: effectiveCwd,
|
cwd: effectiveCwd,
|
||||||
|
type: normalizedType,
|
||||||
initialMode,
|
initialMode,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -892,7 +904,7 @@ export class Session {
|
|||||||
payload: {
|
payload: {
|
||||||
agentId,
|
agentId,
|
||||||
status: info.status,
|
status: info.status,
|
||||||
type: "claude",
|
type: info.type,
|
||||||
currentModeId: info.currentModeId ?? undefined,
|
currentModeId: info.currentModeId ?? undefined,
|
||||||
availableModes: info.availableModes ?? undefined,
|
availableModes: info.availableModes ?? undefined,
|
||||||
title: info.title ?? undefined,
|
title: info.title ?? undefined,
|
||||||
@@ -1419,7 +1431,7 @@ export class Session {
|
|||||||
payload: {
|
payload: {
|
||||||
agentId,
|
agentId,
|
||||||
status: agentInfo.status,
|
status: agentInfo.status,
|
||||||
type: "claude",
|
type: agentInfo.type,
|
||||||
currentModeId: agentInfo.currentModeId ?? undefined,
|
currentModeId: agentInfo.currentModeId ?? undefined,
|
||||||
availableModes: agentInfo.availableModes ?? undefined,
|
availableModes: agentInfo.availableModes ?? undefined,
|
||||||
title: agentInfo.title || undefined,
|
title: agentInfo.title || undefined,
|
||||||
|
|||||||
Reference in New Issue
Block a user