@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.
Files changed (68) hide show
  1. package/dist/commands/db.d.ts.map +1 -1
  2. package/dist/commands/db.js +18 -6
  3. package/dist/commands/db.js.map +1 -1
  4. package/dist/commands/deploy.d.ts.map +1 -1
  5. package/dist/commands/deploy.js +191 -80
  6. package/dist/commands/deploy.js.map +1 -1
  7. package/dist/commands/dev.js +3 -2
  8. package/dist/commands/dev.js.map +1 -1
  9. package/dist/commands/init.d.ts.map +1 -1
  10. package/dist/commands/init.js +12 -0
  11. package/dist/commands/init.js.map +1 -1
  12. package/dist/types.d.ts +1 -1
  13. package/dist/types.d.ts.map +1 -1
  14. package/dist/utils/app-template-sync.d.ts.map +1 -1
  15. package/dist/utils/app-template-sync.js +35 -13
  16. package/dist/utils/app-template-sync.js.map +1 -1
  17. package/dist/utils/init/quickstart-setup.d.ts.map +1 -1
  18. package/dist/utils/init/quickstart-setup.js.map +1 -1
  19. package/package.json +4 -4
  20. package/templates/basic/.env.local.example +23 -0
  21. package/templates/basic/README.md +181 -56
  22. package/templates/basic/package-lock.json +2180 -406
  23. package/templates/basic/package.json +23 -5
  24. package/templates/basic/src/__tests__/chat.test.ts +340 -0
  25. package/templates/basic/src/__tests__/cortex.test.ts +260 -0
  26. package/templates/basic/src/__tests__/display.test.ts +455 -0
  27. package/templates/basic/src/__tests__/e2e/fact-extraction.test.ts +498 -0
  28. package/templates/basic/src/__tests__/e2e/memory-flow.test.ts +355 -0
  29. package/templates/basic/src/__tests__/e2e/server-e2e.test.ts +414 -0
  30. package/templates/basic/src/__tests__/helpers/test-utils.ts +345 -0
  31. package/templates/basic/src/__tests__/integration/chat-flow.test.ts +422 -0
  32. package/templates/basic/src/__tests__/integration/server.test.ts +441 -0
  33. package/templates/basic/src/__tests__/llm.test.ts +344 -0
  34. package/templates/basic/src/chat.ts +300 -0
  35. package/templates/basic/src/cortex.ts +203 -0
  36. package/templates/basic/src/display.ts +425 -0
  37. package/templates/basic/src/index.ts +194 -64
  38. package/templates/basic/src/llm.ts +214 -0
  39. package/templates/basic/src/server.ts +280 -0
  40. package/templates/basic/vitest.config.ts +33 -0
  41. package/templates/basic/vitest.e2e.config.ts +28 -0
  42. package/templates/basic/vitest.integration.config.ts +25 -0
  43. package/templates/vercel-ai-quickstart/app/api/auth/check/route.ts +1 -1
  44. package/templates/vercel-ai-quickstart/app/api/auth/login/route.ts +61 -19
  45. package/templates/vercel-ai-quickstart/app/api/auth/register/route.ts +14 -18
  46. package/templates/vercel-ai-quickstart/app/api/auth/setup/route.ts +4 -7
  47. package/templates/vercel-ai-quickstart/app/api/chat/route.ts +95 -23
  48. package/templates/vercel-ai-quickstart/app/api/chat-v6/route.ts +339 -0
  49. package/templates/vercel-ai-quickstart/app/api/conversations/route.ts +16 -16
  50. package/templates/vercel-ai-quickstart/app/globals.css +24 -9
  51. package/templates/vercel-ai-quickstart/app/page.tsx +41 -15
  52. package/templates/vercel-ai-quickstart/components/AdminSetup.tsx +3 -1
  53. package/templates/vercel-ai-quickstart/components/AuthProvider.tsx +6 -6
  54. package/templates/vercel-ai-quickstart/components/ChatHistorySidebar.tsx +19 -8
  55. package/templates/vercel-ai-quickstart/components/ChatInterface.tsx +46 -16
  56. package/templates/vercel-ai-quickstart/components/LoginScreen.tsx +10 -5
  57. package/templates/vercel-ai-quickstart/jest.config.js +8 -1
  58. package/templates/vercel-ai-quickstart/lib/agents/memory-agent.ts +165 -0
  59. package/templates/vercel-ai-quickstart/lib/password.ts +5 -5
  60. package/templates/vercel-ai-quickstart/lib/versions.ts +60 -0
  61. package/templates/vercel-ai-quickstart/next.config.js +10 -2
  62. package/templates/vercel-ai-quickstart/package.json +23 -12
  63. package/templates/vercel-ai-quickstart/test-api.mjs +303 -0
  64. package/templates/vercel-ai-quickstart/tests/e2e/chat-memory-flow.test.ts +483 -0
  65. package/templates/vercel-ai-quickstart/tests/helpers/mock-cortex.ts +40 -40
  66. package/templates/vercel-ai-quickstart/tests/integration/auth.test.ts +8 -8
  67. package/templates/vercel-ai-quickstart/tests/integration/conversations.test.ts +12 -8
  68. 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<string | null>(null);
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((conversationId: string) => {
71
- setCurrentConversationId(conversationId);
72
- resetLayers();
73
- }, [resetLayers]);
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((conversationId: string) => {
77
- // Update current conversation ID if it was null (new chat created)
78
- if (!currentConversationId) {
79
- setCurrentConversationId(conversationId);
80
- }
81
- }, [currentConversationId]);
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="min-h-screen flex flex-col">
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>Cortex SDK v0.24.0</span>
206
+ <span>
207
+ Cortex SDK {versions ? `v${versions.cortexSdk}` : "..."}
208
+ </span>
189
209
  <span>•</span>
190
- <span>Vercel AI SDK v5</span>
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">Set up your admin password to get started</p>
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(conversations: Conversation[]): Record<string, Conversation[]> {
29
+ function groupByDate(
30
+ conversations: Conversation[],
31
+ ): Record<string, Conversation[]> {
30
32
  const now = new Date();
31
- const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
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(`/api/conversations?conversationId=${encodeURIComponent(conversationId)}`, {
125
- method: "DELETE",
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(data.error || `Delete failed with status ${response.status}`);
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: "/api/chat",
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
- // Clear messages first
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((msg: { id: string; role: string; content: string; createdAt: string }) => ({
148
- id: msg.id,
149
- role: msg.role,
150
- content: msg.content,
151
- createdAt: new Date(msg.createdAt),
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: { content?: string; parts?: Array<{ type: string; text?: string }> }): string => {
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">Loading conversation...</span>
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 ? "Continue your conversation" : "Start a new conversation"}
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">Sign in to explore AI with long-term memory</p>
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={activeTab === "login" ? "current-password" : "new-password"}
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" ? "Signing in..." : "Creating account..."}
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
- * Two test projects:
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