@cortexmemory/cli 0.27.3 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/db.d.ts.map +1 -1
- package/dist/commands/db.js +18 -6
- package/dist/commands/db.js.map +1 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +191 -80
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/dev.js +3 -2
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +12 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/app-template-sync.d.ts.map +1 -1
- package/dist/utils/app-template-sync.js +35 -13
- package/dist/utils/app-template-sync.js.map +1 -1
- package/dist/utils/init/quickstart-setup.d.ts.map +1 -1
- package/dist/utils/init/quickstart-setup.js.map +1 -1
- package/package.json +4 -4
- package/templates/basic/.env.local.example +23 -0
- package/templates/basic/README.md +181 -56
- package/templates/basic/package-lock.json +2180 -406
- package/templates/basic/package.json +23 -5
- package/templates/basic/src/__tests__/chat.test.ts +340 -0
- package/templates/basic/src/__tests__/cortex.test.ts +260 -0
- package/templates/basic/src/__tests__/display.test.ts +455 -0
- package/templates/basic/src/__tests__/e2e/fact-extraction.test.ts +498 -0
- package/templates/basic/src/__tests__/e2e/memory-flow.test.ts +355 -0
- package/templates/basic/src/__tests__/e2e/server-e2e.test.ts +414 -0
- package/templates/basic/src/__tests__/helpers/test-utils.ts +345 -0
- package/templates/basic/src/__tests__/integration/chat-flow.test.ts +422 -0
- package/templates/basic/src/__tests__/integration/server.test.ts +441 -0
- package/templates/basic/src/__tests__/llm.test.ts +344 -0
- package/templates/basic/src/chat.ts +300 -0
- package/templates/basic/src/cortex.ts +203 -0
- package/templates/basic/src/display.ts +425 -0
- package/templates/basic/src/index.ts +194 -64
- package/templates/basic/src/llm.ts +214 -0
- package/templates/basic/src/server.ts +280 -0
- package/templates/basic/vitest.config.ts +33 -0
- package/templates/basic/vitest.e2e.config.ts +28 -0
- package/templates/basic/vitest.integration.config.ts +25 -0
- package/templates/vercel-ai-quickstart/app/api/auth/check/route.ts +1 -1
- package/templates/vercel-ai-quickstart/app/api/auth/login/route.ts +61 -19
- package/templates/vercel-ai-quickstart/app/api/auth/register/route.ts +14 -18
- package/templates/vercel-ai-quickstart/app/api/auth/setup/route.ts +4 -7
- package/templates/vercel-ai-quickstart/app/api/chat/route.ts +95 -23
- package/templates/vercel-ai-quickstart/app/api/chat-v6/route.ts +339 -0
- package/templates/vercel-ai-quickstart/app/api/conversations/route.ts +16 -16
- package/templates/vercel-ai-quickstart/app/globals.css +24 -9
- package/templates/vercel-ai-quickstart/app/page.tsx +41 -15
- package/templates/vercel-ai-quickstart/components/AdminSetup.tsx +3 -1
- package/templates/vercel-ai-quickstart/components/AuthProvider.tsx +6 -6
- package/templates/vercel-ai-quickstart/components/ChatHistorySidebar.tsx +19 -8
- package/templates/vercel-ai-quickstart/components/ChatInterface.tsx +46 -16
- package/templates/vercel-ai-quickstart/components/LoginScreen.tsx +10 -5
- package/templates/vercel-ai-quickstart/jest.config.js +8 -1
- package/templates/vercel-ai-quickstart/lib/agents/memory-agent.ts +165 -0
- package/templates/vercel-ai-quickstart/lib/password.ts +5 -5
- package/templates/vercel-ai-quickstart/lib/versions.ts +60 -0
- package/templates/vercel-ai-quickstart/next.config.js +10 -2
- package/templates/vercel-ai-quickstart/package.json +23 -12
- package/templates/vercel-ai-quickstart/test-api.mjs +303 -0
- package/templates/vercel-ai-quickstart/tests/e2e/chat-memory-flow.test.ts +483 -0
- package/templates/vercel-ai-quickstart/tests/helpers/mock-cortex.ts +40 -40
- package/templates/vercel-ai-quickstart/tests/integration/auth.test.ts +8 -8
- package/templates/vercel-ai-quickstart/tests/integration/conversations.test.ts +12 -8
- package/templates/vercel-ai-quickstart/tests/unit/password.test.ts +4 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import dynamic from "next/dynamic";
|
|
4
|
-
import { useState, useCallback } from "react";
|
|
4
|
+
import { useState, useCallback, useEffect } from "react";
|
|
5
5
|
import { useLayerTracking } from "@/lib/layer-tracking";
|
|
6
|
+
import { detectVersions, type VersionInfo } from "@/lib/versions";
|
|
6
7
|
import { AuthProvider, useAuth } from "@/components/AuthProvider";
|
|
7
8
|
import { AdminSetup } from "@/components/AdminSetup";
|
|
8
9
|
import { LoginScreen } from "@/components/LoginScreen";
|
|
@@ -51,7 +52,10 @@ const ChatHistorySidebar = dynamic(
|
|
|
51
52
|
function MainContent() {
|
|
52
53
|
const { isLoading, isAdminSetup, isAuthenticated, user } = useAuth();
|
|
53
54
|
const [memorySpaceId, setMemorySpaceId] = useState("quickstart-demo");
|
|
54
|
-
const [currentConversationId, setCurrentConversationId] = useState<
|
|
55
|
+
const [currentConversationId, setCurrentConversationId] = useState<
|
|
56
|
+
string | null
|
|
57
|
+
>(null);
|
|
58
|
+
const [versions, setVersions] = useState<VersionInfo | null>(null);
|
|
55
59
|
const {
|
|
56
60
|
layers,
|
|
57
61
|
isOrchestrating,
|
|
@@ -60,6 +64,11 @@ function MainContent() {
|
|
|
60
64
|
resetLayers,
|
|
61
65
|
} = useLayerTracking();
|
|
62
66
|
|
|
67
|
+
// Detect SDK versions on mount
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
detectVersions().then(setVersions);
|
|
70
|
+
}, []);
|
|
71
|
+
|
|
63
72
|
// Handle new chat
|
|
64
73
|
const handleNewChat = useCallback(() => {
|
|
65
74
|
setCurrentConversationId(null);
|
|
@@ -67,18 +76,24 @@ function MainContent() {
|
|
|
67
76
|
}, [resetLayers]);
|
|
68
77
|
|
|
69
78
|
// Handle conversation selection
|
|
70
|
-
const handleSelectConversation = useCallback(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
79
|
+
const handleSelectConversation = useCallback(
|
|
80
|
+
(conversationId: string) => {
|
|
81
|
+
setCurrentConversationId(conversationId);
|
|
82
|
+
resetLayers();
|
|
83
|
+
},
|
|
84
|
+
[resetLayers],
|
|
85
|
+
);
|
|
74
86
|
|
|
75
87
|
// Handle conversation update (e.g., title change after first message)
|
|
76
|
-
const handleConversationUpdate = useCallback(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
88
|
+
const handleConversationUpdate = useCallback(
|
|
89
|
+
(conversationId: string) => {
|
|
90
|
+
// Update current conversation ID if it was null (new chat created)
|
|
91
|
+
if (!currentConversationId) {
|
|
92
|
+
setCurrentConversationId(conversationId);
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
[currentConversationId],
|
|
96
|
+
);
|
|
82
97
|
|
|
83
98
|
// Loading state
|
|
84
99
|
if (isLoading) {
|
|
@@ -109,7 +124,7 @@ function MainContent() {
|
|
|
109
124
|
|
|
110
125
|
// Main authenticated interface
|
|
111
126
|
return (
|
|
112
|
-
<main className="
|
|
127
|
+
<main className="h-screen flex flex-col overflow-hidden">
|
|
113
128
|
{/* Header */}
|
|
114
129
|
<header className="border-b border-white/10 px-6 py-4">
|
|
115
130
|
<div className="flex items-center justify-between">
|
|
@@ -151,6 +166,9 @@ function MainContent() {
|
|
|
151
166
|
memorySpaceId={memorySpaceId}
|
|
152
167
|
userId={userId}
|
|
153
168
|
conversationId={currentConversationId}
|
|
169
|
+
apiEndpoint={
|
|
170
|
+
versions?.aiSdkMajor === 6 ? "/api/chat-v6" : "/api/chat"
|
|
171
|
+
}
|
|
154
172
|
onOrchestrationStart={startOrchestration}
|
|
155
173
|
onLayerUpdate={updateLayer}
|
|
156
174
|
onReset={resetLayers}
|
|
@@ -185,9 +203,17 @@ function MainContent() {
|
|
|
185
203
|
<footer className="border-t border-white/10 px-6 py-3">
|
|
186
204
|
<div className="flex items-center justify-between text-sm text-gray-500">
|
|
187
205
|
<div className="flex items-center gap-4">
|
|
188
|
-
<span>
|
|
206
|
+
<span>
|
|
207
|
+
Cortex SDK {versions ? `v${versions.cortexSdk}` : "..."}
|
|
208
|
+
</span>
|
|
189
209
|
<span>•</span>
|
|
190
|
-
<span>Vercel AI SDK
|
|
210
|
+
<span>Vercel AI SDK {versions?.aiSdk ?? "..."}</span>
|
|
211
|
+
{versions?.aiSdkMajor === 6 && (
|
|
212
|
+
<>
|
|
213
|
+
<span>•</span>
|
|
214
|
+
<span className="text-cortex-400">Using ToolLoopAgent</span>
|
|
215
|
+
</>
|
|
216
|
+
)}
|
|
191
217
|
</div>
|
|
192
218
|
<a
|
|
193
219
|
href="https://cortexmemory.dev/docs"
|
|
@@ -41,7 +41,9 @@ export function AdminSetup() {
|
|
|
41
41
|
<span className="text-4xl">🧠</span>
|
|
42
42
|
</div>
|
|
43
43
|
<h1 className="text-3xl font-bold mb-2">Welcome to Cortex</h1>
|
|
44
|
-
<p className="text-gray-400">
|
|
44
|
+
<p className="text-gray-400">
|
|
45
|
+
Set up your admin password to get started
|
|
46
|
+
</p>
|
|
45
47
|
</div>
|
|
46
48
|
|
|
47
49
|
{/* Setup Form */}
|
|
@@ -32,7 +32,7 @@ export interface AuthContextValue extends AuthState {
|
|
|
32
32
|
register: (
|
|
33
33
|
username: string,
|
|
34
34
|
password: string,
|
|
35
|
-
displayName?: string
|
|
35
|
+
displayName?: string,
|
|
36
36
|
) => Promise<boolean>;
|
|
37
37
|
logout: () => void;
|
|
38
38
|
clearError: () => void;
|
|
@@ -164,7 +164,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|
|
164
164
|
JSON.stringify({
|
|
165
165
|
user: data.user,
|
|
166
166
|
sessionToken: data.sessionToken,
|
|
167
|
-
})
|
|
167
|
+
}),
|
|
168
168
|
);
|
|
169
169
|
}
|
|
170
170
|
|
|
@@ -184,7 +184,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|
|
184
184
|
return false;
|
|
185
185
|
}
|
|
186
186
|
},
|
|
187
|
-
[]
|
|
187
|
+
[],
|
|
188
188
|
);
|
|
189
189
|
|
|
190
190
|
// Register
|
|
@@ -192,7 +192,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|
|
192
192
|
async (
|
|
193
193
|
username: string,
|
|
194
194
|
password: string,
|
|
195
|
-
displayName?: string
|
|
195
|
+
displayName?: string,
|
|
196
196
|
): Promise<boolean> => {
|
|
197
197
|
setState((prev) => ({ ...prev, error: null }));
|
|
198
198
|
|
|
@@ -217,7 +217,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|
|
217
217
|
JSON.stringify({
|
|
218
218
|
user: data.user,
|
|
219
219
|
sessionToken: data.sessionToken,
|
|
220
|
-
})
|
|
220
|
+
}),
|
|
221
221
|
);
|
|
222
222
|
}
|
|
223
223
|
|
|
@@ -237,7 +237,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|
|
237
237
|
return false;
|
|
238
238
|
}
|
|
239
239
|
},
|
|
240
|
-
[]
|
|
240
|
+
[],
|
|
241
241
|
);
|
|
242
242
|
|
|
243
243
|
// Logout
|
|
@@ -26,9 +26,15 @@ interface ChatHistorySidebarProps {
|
|
|
26
26
|
// Helpers
|
|
27
27
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
28
28
|
|
|
29
|
-
function groupByDate(
|
|
29
|
+
function groupByDate(
|
|
30
|
+
conversations: Conversation[],
|
|
31
|
+
): Record<string, Conversation[]> {
|
|
30
32
|
const now = new Date();
|
|
31
|
-
const today = new Date(
|
|
33
|
+
const today = new Date(
|
|
34
|
+
now.getFullYear(),
|
|
35
|
+
now.getMonth(),
|
|
36
|
+
now.getDate(),
|
|
37
|
+
).getTime();
|
|
32
38
|
const yesterday = today - 86400000;
|
|
33
39
|
const weekAgo = today - 7 * 86400000;
|
|
34
40
|
|
|
@@ -44,7 +50,7 @@ function groupByDate(conversations: Conversation[]): Record<string, Conversation
|
|
|
44
50
|
const convDay = new Date(
|
|
45
51
|
convDate.getFullYear(),
|
|
46
52
|
convDate.getMonth(),
|
|
47
|
-
convDate.getDate()
|
|
53
|
+
convDate.getDate(),
|
|
48
54
|
).getTime();
|
|
49
55
|
|
|
50
56
|
if (convDay >= today) {
|
|
@@ -89,7 +95,7 @@ export function ChatHistorySidebar({
|
|
|
89
95
|
|
|
90
96
|
try {
|
|
91
97
|
const response = await fetch(
|
|
92
|
-
`/api/conversations?userId=${encodeURIComponent(user.id)}&memorySpaceId=${encodeURIComponent(memorySpaceId)}
|
|
98
|
+
`/api/conversations?userId=${encodeURIComponent(user.id)}&memorySpaceId=${encodeURIComponent(memorySpaceId)}`,
|
|
93
99
|
);
|
|
94
100
|
const data = await response.json();
|
|
95
101
|
if (data.conversations) {
|
|
@@ -121,13 +127,18 @@ export function ChatHistorySidebar({
|
|
|
121
127
|
setDeletingId(conversationId);
|
|
122
128
|
|
|
123
129
|
try {
|
|
124
|
-
const response = await fetch(
|
|
125
|
-
|
|
126
|
-
|
|
130
|
+
const response = await fetch(
|
|
131
|
+
`/api/conversations?conversationId=${encodeURIComponent(conversationId)}`,
|
|
132
|
+
{
|
|
133
|
+
method: "DELETE",
|
|
134
|
+
},
|
|
135
|
+
);
|
|
127
136
|
|
|
128
137
|
if (!response.ok) {
|
|
129
138
|
const data = await response.json().catch(() => ({}));
|
|
130
|
-
throw new Error(
|
|
139
|
+
throw new Error(
|
|
140
|
+
data.error || `Delete failed with status ${response.status}`,
|
|
141
|
+
);
|
|
131
142
|
}
|
|
132
143
|
|
|
133
144
|
// Remove from local state only after successful deletion
|
|
@@ -26,6 +26,8 @@ interface ChatInterfaceProps {
|
|
|
26
26
|
memorySpaceId: string;
|
|
27
27
|
userId: string;
|
|
28
28
|
conversationId: string | null;
|
|
29
|
+
/** API endpoint for chat - defaults to /api/chat, use /api/chat-v6 for AI SDK v6 */
|
|
30
|
+
apiEndpoint?: string;
|
|
29
31
|
onOrchestrationStart?: () => void;
|
|
30
32
|
onLayerUpdate?: (
|
|
31
33
|
layer: MemoryLayer,
|
|
@@ -44,6 +46,7 @@ export function ChatInterface({
|
|
|
44
46
|
memorySpaceId,
|
|
45
47
|
userId,
|
|
46
48
|
conversationId,
|
|
49
|
+
apiEndpoint = "/api/chat",
|
|
47
50
|
onOrchestrationStart,
|
|
48
51
|
onLayerUpdate,
|
|
49
52
|
onReset,
|
|
@@ -66,12 +69,15 @@ export function ChatInterface({
|
|
|
66
69
|
conversationIdRef.current = conversationId;
|
|
67
70
|
}, [conversationId]);
|
|
68
71
|
|
|
72
|
+
// Track if we should skip history load (when we create a new conversation ourselves)
|
|
73
|
+
const skipHistoryLoadRef = useRef<string | null>(null);
|
|
74
|
+
|
|
69
75
|
// Create transport with a function that reads from ref for conversationId
|
|
70
76
|
// This ensures we always send the latest conversationId
|
|
71
77
|
const transport = useMemo(
|
|
72
78
|
() =>
|
|
73
79
|
new DefaultChatTransport({
|
|
74
|
-
api:
|
|
80
|
+
api: apiEndpoint,
|
|
75
81
|
// Use a function to get body so it reads latest conversationId from ref
|
|
76
82
|
body: () => ({
|
|
77
83
|
memorySpaceId,
|
|
@@ -79,7 +85,7 @@ export function ChatInterface({
|
|
|
79
85
|
conversationId: conversationIdRef.current,
|
|
80
86
|
}),
|
|
81
87
|
}),
|
|
82
|
-
[memorySpaceId, userId], // Note: conversationId removed - ref handles updates
|
|
88
|
+
[apiEndpoint, memorySpaceId, userId], // Note: conversationId removed - ref handles updates
|
|
83
89
|
);
|
|
84
90
|
|
|
85
91
|
// Handle layer data parts from the stream
|
|
@@ -101,6 +107,8 @@ export function ChatInterface({
|
|
|
101
107
|
// Handle conversation title update
|
|
102
108
|
if (part.type === "data-conversation-update") {
|
|
103
109
|
const update = part.data as { conversationId: string; title: string };
|
|
110
|
+
// Mark this conversation ID to skip history load - we just created it
|
|
111
|
+
skipHistoryLoadRef.current = update.conversationId;
|
|
104
112
|
onConversationUpdate?.(update.conversationId, update.title);
|
|
105
113
|
}
|
|
106
114
|
},
|
|
@@ -119,20 +127,28 @@ export function ChatInterface({
|
|
|
119
127
|
|
|
120
128
|
// Load messages when conversation changes
|
|
121
129
|
useEffect(() => {
|
|
122
|
-
//
|
|
123
|
-
setMessages([]);
|
|
124
|
-
|
|
125
|
-
// If no conversation selected, nothing more to do
|
|
130
|
+
// If no conversation selected, just clear messages
|
|
126
131
|
if (!conversationId) {
|
|
132
|
+
setMessages([]);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Skip loading if we just created this conversation ourselves
|
|
137
|
+
// (we already have the messages in state from the current chat session)
|
|
138
|
+
if (skipHistoryLoadRef.current === conversationId) {
|
|
139
|
+
skipHistoryLoadRef.current = null;
|
|
127
140
|
return;
|
|
128
141
|
}
|
|
129
142
|
|
|
143
|
+
// Clear messages before loading new conversation
|
|
144
|
+
setMessages([]);
|
|
145
|
+
|
|
130
146
|
// Fetch conversation history
|
|
131
147
|
const loadConversationHistory = async () => {
|
|
132
148
|
setIsLoadingHistory(true);
|
|
133
149
|
try {
|
|
134
150
|
const response = await fetch(
|
|
135
|
-
`/api/conversations?conversationId=${encodeURIComponent(conversationId)}
|
|
151
|
+
`/api/conversations?conversationId=${encodeURIComponent(conversationId)}`,
|
|
136
152
|
);
|
|
137
153
|
|
|
138
154
|
if (!response.ok) {
|
|
@@ -144,12 +160,19 @@ export function ChatInterface({
|
|
|
144
160
|
|
|
145
161
|
if (data.messages && data.messages.length > 0) {
|
|
146
162
|
// Transform to the format expected by useChat
|
|
147
|
-
const loadedMessages = data.messages.map(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
163
|
+
const loadedMessages = data.messages.map(
|
|
164
|
+
(msg: {
|
|
165
|
+
id: string;
|
|
166
|
+
role: string;
|
|
167
|
+
content: string;
|
|
168
|
+
createdAt: string;
|
|
169
|
+
}) => ({
|
|
170
|
+
id: msg.id,
|
|
171
|
+
role: msg.role,
|
|
172
|
+
content: msg.content,
|
|
173
|
+
createdAt: new Date(msg.createdAt),
|
|
174
|
+
}),
|
|
175
|
+
);
|
|
153
176
|
setMessages(loadedMessages);
|
|
154
177
|
}
|
|
155
178
|
} catch (error) {
|
|
@@ -190,7 +213,10 @@ export function ChatInterface({
|
|
|
190
213
|
};
|
|
191
214
|
|
|
192
215
|
// Extract text content from message parts (AI SDK v5 format)
|
|
193
|
-
const getMessageContent = (message: {
|
|
216
|
+
const getMessageContent = (message: {
|
|
217
|
+
content?: string;
|
|
218
|
+
parts?: Array<{ type: string; text?: string }>;
|
|
219
|
+
}): string => {
|
|
194
220
|
if (typeof message.content === "string") {
|
|
195
221
|
return message.content;
|
|
196
222
|
}
|
|
@@ -230,7 +256,9 @@ export function ChatInterface({
|
|
|
230
256
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
|
231
257
|
/>
|
|
232
258
|
</svg>
|
|
233
|
-
<span className="text-gray-400 text-sm">
|
|
259
|
+
<span className="text-gray-400 text-sm">
|
|
260
|
+
Loading conversation...
|
|
261
|
+
</span>
|
|
234
262
|
</div>
|
|
235
263
|
</div>
|
|
236
264
|
)}
|
|
@@ -241,7 +269,9 @@ export function ChatInterface({
|
|
|
241
269
|
<span className="text-3xl">🧠</span>
|
|
242
270
|
</div>
|
|
243
271
|
<h2 className="text-xl font-semibold mb-2">
|
|
244
|
-
{conversationId
|
|
272
|
+
{conversationId
|
|
273
|
+
? "Continue your conversation"
|
|
274
|
+
: "Start a new conversation"}
|
|
245
275
|
</h2>
|
|
246
276
|
<p className="text-gray-400 max-w-md mx-auto mb-6">
|
|
247
277
|
This demo shows how Cortex orchestrates memory across multiple
|
|
@@ -57,7 +57,9 @@ export function LoginScreen() {
|
|
|
57
57
|
<span className="text-4xl">🧠</span>
|
|
58
58
|
</div>
|
|
59
59
|
<h1 className="text-3xl font-bold mb-2">Cortex Memory Demo</h1>
|
|
60
|
-
<p className="text-gray-400">
|
|
60
|
+
<p className="text-gray-400">
|
|
61
|
+
Sign in to explore AI with long-term memory
|
|
62
|
+
</p>
|
|
61
63
|
</div>
|
|
62
64
|
|
|
63
65
|
{/* Login/Register Card */}
|
|
@@ -114,8 +116,7 @@ export function LoginScreen() {
|
|
|
114
116
|
htmlFor="displayName"
|
|
115
117
|
className="block text-sm font-medium text-gray-300 mb-2"
|
|
116
118
|
>
|
|
117
|
-
Display Name
|
|
118
|
-
<span className="text-gray-500">(optional)</span>
|
|
119
|
+
Display Name <span className="text-gray-500">(optional)</span>
|
|
119
120
|
</label>
|
|
120
121
|
<input
|
|
121
122
|
id="displayName"
|
|
@@ -144,7 +145,9 @@ export function LoginScreen() {
|
|
|
144
145
|
placeholder="Enter your password"
|
|
145
146
|
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl focus:outline-none focus:border-cortex-500 transition-colors"
|
|
146
147
|
disabled={isLoading}
|
|
147
|
-
autoComplete={
|
|
148
|
+
autoComplete={
|
|
149
|
+
activeTab === "login" ? "current-password" : "new-password"
|
|
150
|
+
}
|
|
148
151
|
/>
|
|
149
152
|
</div>
|
|
150
153
|
|
|
@@ -181,7 +184,9 @@ export function LoginScreen() {
|
|
|
181
184
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
|
182
185
|
/>
|
|
183
186
|
</svg>
|
|
184
|
-
{activeTab === "login"
|
|
187
|
+
{activeTab === "login"
|
|
188
|
+
? "Signing in..."
|
|
189
|
+
: "Creating account..."}
|
|
185
190
|
</>
|
|
186
191
|
) : activeTab === "login" ? (
|
|
187
192
|
"Sign In"
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Jest Configuration for Vercel AI Quickstart
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Three test projects:
|
|
5
5
|
* - unit: Fast unit tests with mocked dependencies
|
|
6
6
|
* - integration: Integration tests for API routes with mocked SDK
|
|
7
|
+
* - e2e: End-to-end tests with real Cortex backend (requires CONVEX_URL, OPENAI_API_KEY)
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
const baseConfig = {
|
|
@@ -41,5 +42,11 @@ module.exports = {
|
|
|
41
42
|
testMatch: ["<rootDir>/tests/integration/**/*.test.ts"],
|
|
42
43
|
testTimeout: 30000,
|
|
43
44
|
},
|
|
45
|
+
{
|
|
46
|
+
...baseConfig,
|
|
47
|
+
displayName: "e2e",
|
|
48
|
+
testMatch: ["<rootDir>/tests/e2e/**/*.test.ts"],
|
|
49
|
+
testTimeout: 120000, // 2 minutes for real network calls
|
|
50
|
+
},
|
|
44
51
|
],
|
|
45
52
|
};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory-Enabled Agent (AI SDK v6)
|
|
3
|
+
*
|
|
4
|
+
* This file demonstrates how to create a reusable agent with
|
|
5
|
+
* Cortex Memory integration using AI SDK v6's ToolLoopAgent.
|
|
6
|
+
*
|
|
7
|
+
* The agent:
|
|
8
|
+
* - Automatically injects relevant memories into context
|
|
9
|
+
* - Can be used with both generate() and stream()
|
|
10
|
+
* - Supports type-safe call options for userId, memorySpaceId, etc.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const result = await memoryAgent.generate({
|
|
15
|
+
* prompt: 'What do you remember about me?',
|
|
16
|
+
* options: {
|
|
17
|
+
* userId: 'user_123',
|
|
18
|
+
* memorySpaceId: 'my-app',
|
|
19
|
+
* },
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { ToolLoopAgent, tool, stepCountIs } from "ai";
|
|
25
|
+
import { openai } from "@ai-sdk/openai";
|
|
26
|
+
import { z } from "zod";
|
|
27
|
+
import {
|
|
28
|
+
createCortexCallOptionsSchema,
|
|
29
|
+
createMemoryPrepareCall,
|
|
30
|
+
type CortexCallOptions,
|
|
31
|
+
} from "@cortexmemory/vercel-ai-provider";
|
|
32
|
+
|
|
33
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
34
|
+
// Agent Configuration
|
|
35
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
36
|
+
|
|
37
|
+
const SYSTEM_PROMPT = `You are a helpful AI assistant with long-term memory powered by Cortex.
|
|
38
|
+
|
|
39
|
+
Your capabilities:
|
|
40
|
+
- You remember everything users tell you across conversations
|
|
41
|
+
- You can recall facts, preferences, and context from past interactions
|
|
42
|
+
- You naturally reference what you've learned about the user
|
|
43
|
+
|
|
44
|
+
Behavior guidelines:
|
|
45
|
+
- When you remember something from a previous conversation, mention it naturally
|
|
46
|
+
- If asked about something you learned, reference it specifically
|
|
47
|
+
- Be conversational and friendly
|
|
48
|
+
- Help demonstrate the memory system by showing what you remember`;
|
|
49
|
+
|
|
50
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
51
|
+
// Memory Agent Definition
|
|
52
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A memory-enabled agent using AI SDK v6's ToolLoopAgent.
|
|
56
|
+
*
|
|
57
|
+
* This agent demonstrates:
|
|
58
|
+
* - callOptionsSchema for type-safe runtime config (userId, memorySpaceId, etc.)
|
|
59
|
+
* - prepareCall for automatic memory context injection via Cortex's recall() API
|
|
60
|
+
* - Built-in tools for memory operations (optional)
|
|
61
|
+
*
|
|
62
|
+
* The callOptionsSchema ensures TypeScript type safety when calling the agent:
|
|
63
|
+
* - userId: required for memory isolation per user
|
|
64
|
+
* - memorySpaceId: required for data partitioning
|
|
65
|
+
* - conversationId: optional for session continuity
|
|
66
|
+
* - agentId: optional agent identifier
|
|
67
|
+
*/
|
|
68
|
+
export const memoryAgent = new ToolLoopAgent({
|
|
69
|
+
id: "cortex-memory-agent",
|
|
70
|
+
model: openai("gpt-4o-mini"),
|
|
71
|
+
instructions: SYSTEM_PROMPT,
|
|
72
|
+
|
|
73
|
+
// ┌─────────────────────────────────────────────────────────────────┐
|
|
74
|
+
// │ callOptionsSchema: Type-Safe Runtime Configuration │
|
|
75
|
+
// │ │
|
|
76
|
+
// │ This Zod schema defines what options must/can be passed when │
|
|
77
|
+
// │ calling the agent. AI SDK v6 validates these at runtime. │
|
|
78
|
+
// │ │
|
|
79
|
+
// │ Example usage: │
|
|
80
|
+
// │ await memoryAgent.generate({ │
|
|
81
|
+
// │ prompt: 'Hello!', │
|
|
82
|
+
// │ options: { userId: 'u1', memorySpaceId: 'app1' }, // typed!│
|
|
83
|
+
// │ }); │
|
|
84
|
+
// └─────────────────────────────────────────────────────────────────┘
|
|
85
|
+
callOptionsSchema: createCortexCallOptionsSchema(),
|
|
86
|
+
|
|
87
|
+
// ┌─────────────────────────────────────────────────────────────────┐
|
|
88
|
+
// │ prepareCall: Memory Context Injection │
|
|
89
|
+
// │ │
|
|
90
|
+
// │ Called before each agent invocation. This hook: │
|
|
91
|
+
// │ 1. Extracts the user's query from messages │
|
|
92
|
+
// │ 2. Calls Cortex memory.recall() with userId + memorySpaceId │
|
|
93
|
+
// │ 3. Injects the returned context into instructions │
|
|
94
|
+
// │ │
|
|
95
|
+
// │ The recall() API orchestrates all memory layers: │
|
|
96
|
+
// │ - Vector memories (semantic search) │
|
|
97
|
+
// │ - Facts (extracted knowledge) │
|
|
98
|
+
// │ - Graph relationships (if configured) │
|
|
99
|
+
// └─────────────────────────────────────────────────────────────────┘
|
|
100
|
+
prepareCall: createMemoryPrepareCall({
|
|
101
|
+
convexUrl: process.env.CONVEX_URL!,
|
|
102
|
+
maxMemories: 20, // Max items to inject from recall
|
|
103
|
+
includeFacts: true, // Include Layer 3 facts
|
|
104
|
+
includeVector: true, // Include Layer 2 vector memories
|
|
105
|
+
includeGraph: true, // Expand through graph relationships
|
|
106
|
+
}),
|
|
107
|
+
|
|
108
|
+
// Default to 5 steps (sufficient for most chat interactions)
|
|
109
|
+
stopWhen: stepCountIs(5),
|
|
110
|
+
|
|
111
|
+
// Optional: Add memory-specific tools for explicit memory operations
|
|
112
|
+
// Uncomment to let the agent actively search/store memories
|
|
113
|
+
/*
|
|
114
|
+
tools: {
|
|
115
|
+
searchMemory: tool({
|
|
116
|
+
description: 'Search for specific memories about the user',
|
|
117
|
+
inputSchema: z.object({
|
|
118
|
+
query: z.string().describe('What to search for in memory'),
|
|
119
|
+
}),
|
|
120
|
+
execute: async ({ query }, { options }) => {
|
|
121
|
+
const { Cortex } = await import('@cortexmemory/sdk');
|
|
122
|
+
const cortex = new Cortex({ convexUrl: process.env.CONVEX_URL! });
|
|
123
|
+
const result = await cortex.memory.recall({
|
|
124
|
+
memorySpaceId: options.memorySpaceId,
|
|
125
|
+
query,
|
|
126
|
+
userId: options.userId,
|
|
127
|
+
limit: 5,
|
|
128
|
+
});
|
|
129
|
+
return result.context || 'No memories found.';
|
|
130
|
+
},
|
|
131
|
+
}),
|
|
132
|
+
},
|
|
133
|
+
*/
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
137
|
+
// Type Exports for Client Components
|
|
138
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Inferred UIMessage type for this agent.
|
|
142
|
+
*
|
|
143
|
+
* Use this in your client components for full type safety:
|
|
144
|
+
*
|
|
145
|
+
* ```typescript
|
|
146
|
+
* import { useChat } from '@ai-sdk/react';
|
|
147
|
+
* import type { MemoryAgentUIMessage } from '@/lib/agents/memory-agent';
|
|
148
|
+
*
|
|
149
|
+
* const { messages } = useChat<MemoryAgentUIMessage>();
|
|
150
|
+
* ```
|
|
151
|
+
*/
|
|
152
|
+
export type MemoryAgentUIMessage = {
|
|
153
|
+
id: string;
|
|
154
|
+
role: "user" | "assistant" | "system";
|
|
155
|
+
createdAt?: Date;
|
|
156
|
+
parts?: Array<
|
|
157
|
+
| { type: "text"; text: string }
|
|
158
|
+
| { type: "tool-invocation"; toolCallId: string; state: string }
|
|
159
|
+
>;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Re-export call options type for convenience.
|
|
164
|
+
*/
|
|
165
|
+
export type { CortexCallOptions };
|
|
@@ -28,7 +28,7 @@ export async function hashPassword(password: string): Promise<string> {
|
|
|
28
28
|
passwordBuffer,
|
|
29
29
|
"PBKDF2",
|
|
30
30
|
false,
|
|
31
|
-
["deriveBits"]
|
|
31
|
+
["deriveBits"],
|
|
32
32
|
);
|
|
33
33
|
|
|
34
34
|
// Derive key using PBKDF2
|
|
@@ -40,7 +40,7 @@ export async function hashPassword(password: string): Promise<string> {
|
|
|
40
40
|
hash: "SHA-256",
|
|
41
41
|
},
|
|
42
42
|
keyMaterial,
|
|
43
|
-
KEY_LENGTH
|
|
43
|
+
KEY_LENGTH,
|
|
44
44
|
);
|
|
45
45
|
|
|
46
46
|
// Convert to base64 strings
|
|
@@ -60,7 +60,7 @@ export async function hashPassword(password: string): Promise<string> {
|
|
|
60
60
|
*/
|
|
61
61
|
export async function verifyPassword(
|
|
62
62
|
password: string,
|
|
63
|
-
storedHash: string
|
|
63
|
+
storedHash: string,
|
|
64
64
|
): Promise<boolean> {
|
|
65
65
|
try {
|
|
66
66
|
const [saltB64, expectedHashB64] = storedHash.split(":");
|
|
@@ -84,7 +84,7 @@ export async function verifyPassword(
|
|
|
84
84
|
passwordBuffer,
|
|
85
85
|
"PBKDF2",
|
|
86
86
|
false,
|
|
87
|
-
["deriveBits"]
|
|
87
|
+
["deriveBits"],
|
|
88
88
|
);
|
|
89
89
|
|
|
90
90
|
// Derive key using same parameters
|
|
@@ -96,7 +96,7 @@ export async function verifyPassword(
|
|
|
96
96
|
hash: "SHA-256",
|
|
97
97
|
},
|
|
98
98
|
keyMaterial,
|
|
99
|
-
KEY_LENGTH
|
|
99
|
+
KEY_LENGTH,
|
|
100
100
|
);
|
|
101
101
|
|
|
102
102
|
// Compare hashes
|