@nextsparkjs/plugin-langchain 0.1.0-beta.1

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 (67) hide show
  1. package/.env.example +41 -0
  2. package/api/observability/metrics/route.ts +110 -0
  3. package/api/observability/traces/[traceId]/route.ts +398 -0
  4. package/api/observability/traces/route.ts +205 -0
  5. package/api/sessions/route.ts +332 -0
  6. package/components/observability/CollapsibleJson.tsx +71 -0
  7. package/components/observability/CompactTimeline.tsx +75 -0
  8. package/components/observability/ConversationFlow.tsx +271 -0
  9. package/components/observability/DisabledMessage.tsx +21 -0
  10. package/components/observability/FiltersPanel.tsx +82 -0
  11. package/components/observability/ObservabilityDashboard.tsx +230 -0
  12. package/components/observability/SpansList.tsx +210 -0
  13. package/components/observability/TraceDetail.tsx +335 -0
  14. package/components/observability/TraceStatusBadge.tsx +39 -0
  15. package/components/observability/TracesTable.tsx +97 -0
  16. package/components/observability/index.ts +7 -0
  17. package/docs/01-getting-started/01-overview.md +196 -0
  18. package/docs/01-getting-started/02-installation.md +368 -0
  19. package/docs/01-getting-started/03-configuration.md +794 -0
  20. package/docs/02-core-concepts/01-architecture.md +566 -0
  21. package/docs/02-core-concepts/02-agents.md +597 -0
  22. package/docs/02-core-concepts/03-tools.md +689 -0
  23. package/docs/03-orchestration/01-graph-orchestrator.md +809 -0
  24. package/docs/03-orchestration/02-legacy-react.md +650 -0
  25. package/docs/04-advanced/01-observability.md +645 -0
  26. package/docs/04-advanced/02-token-tracking.md +469 -0
  27. package/docs/04-advanced/03-streaming.md +476 -0
  28. package/docs/04-advanced/04-guardrails.md +597 -0
  29. package/docs/05-reference/01-api-reference.md +1403 -0
  30. package/docs/05-reference/02-customization.md +646 -0
  31. package/docs/05-reference/03-examples.md +881 -0
  32. package/docs/index.md +85 -0
  33. package/hooks/observability/useMetrics.ts +31 -0
  34. package/hooks/observability/useTraceDetail.ts +48 -0
  35. package/hooks/observability/useTraces.ts +59 -0
  36. package/lib/agent-factory.ts +354 -0
  37. package/lib/agent-helpers.ts +201 -0
  38. package/lib/db-memory-store.ts +417 -0
  39. package/lib/graph/index.ts +58 -0
  40. package/lib/graph/nodes/combiner.ts +399 -0
  41. package/lib/graph/nodes/router.ts +440 -0
  42. package/lib/graph/orchestrator-graph.ts +386 -0
  43. package/lib/graph/prompts/combiner.md +131 -0
  44. package/lib/graph/prompts/router.md +193 -0
  45. package/lib/graph/types.ts +365 -0
  46. package/lib/guardrails.ts +230 -0
  47. package/lib/index.ts +44 -0
  48. package/lib/logger.ts +70 -0
  49. package/lib/memory-store.ts +168 -0
  50. package/lib/message-serializer.ts +110 -0
  51. package/lib/prompt-renderer.ts +94 -0
  52. package/lib/providers.ts +226 -0
  53. package/lib/streaming.ts +232 -0
  54. package/lib/token-tracker.ts +298 -0
  55. package/lib/tools-builder.ts +192 -0
  56. package/lib/tracer-callbacks.ts +342 -0
  57. package/lib/tracer.ts +350 -0
  58. package/migrations/001_langchain_memory.sql +83 -0
  59. package/migrations/002_token_usage.sql +127 -0
  60. package/migrations/003_observability.sql +257 -0
  61. package/package.json +28 -0
  62. package/plugin.config.ts +170 -0
  63. package/presets/lib/langchain.config.ts.preset +142 -0
  64. package/presets/templates/sector7/ai-observability/[traceId]/page.tsx +91 -0
  65. package/presets/templates/sector7/ai-observability/page.tsx +54 -0
  66. package/types/langchain.types.ts +274 -0
  67. package/types/observability.types.ts +270 -0
@@ -0,0 +1,201 @@
1
+ /**
2
+ * ============================================================================
3
+ * AGENT HELPERS FACTORY
4
+ * ============================================================================
5
+ *
6
+ * Provides utility functions for working with agent configurations.
7
+ * Themes define their agents in langchain.config.ts, then use these helpers.
8
+ *
9
+ * USAGE:
10
+ * ```typescript
11
+ * // In theme's langchain.config.ts
12
+ * import { createAgentHelpers } from '@/plugins/langchain/lib/agent-helpers'
13
+ *
14
+ * export const AGENTS = { ... }
15
+ * export const helpers = createAgentHelpers(AGENTS)
16
+ * // or destructure: export const { getAgentConfig, getAgentTools } = createAgentHelpers(AGENTS)
17
+ * ```
18
+ *
19
+ * ============================================================================
20
+ */
21
+
22
+ import type {
23
+ AgentDefinition,
24
+ AgentContext,
25
+ SessionConfig,
26
+ ToolDefinition,
27
+ ThemeLangChainConfig,
28
+ } from '../types/langchain.types'
29
+
30
+ /**
31
+ * Agent helpers interface - all utilities for working with agent configs
32
+ */
33
+ export interface AgentHelpers {
34
+ /** Get complete configuration for an agent */
35
+ getAgentConfig: (agentName: string) => AgentDefinition | undefined
36
+
37
+ /** Get model configuration (provider, model, temperature) */
38
+ getAgentModelConfig: (agentName: string) => Partial<{
39
+ provider: 'openai' | 'anthropic' | 'ollama'
40
+ model: string
41
+ temperature: number
42
+ }> | undefined
43
+
44
+ /** Get tools for an agent with runtime context */
45
+ getAgentTools: (agentName: string, context: AgentContext) => ToolDefinition<any>[]
46
+
47
+ /** Get system prompt name (to be loaded from .md file) */
48
+ getAgentPromptName: (agentName: string) => string | undefined
49
+
50
+ /** Get session configuration for an agent (TTL, maxMessages) */
51
+ getAgentSessionConfig: (agentName: string) => SessionConfig | undefined
52
+
53
+ /** Get enrichContext function for an agent */
54
+ getAgentEnrichContext: (agentName: string) => ((context: AgentContext) => Promise<AgentContext>) | undefined
55
+
56
+ /** Check if an agent is configured */
57
+ hasAgent: (agentName: string) => boolean
58
+
59
+ /** Get all configured agent names */
60
+ getAgentNames: () => string[]
61
+ }
62
+
63
+ /**
64
+ * Create agent helper functions bound to a specific agents configuration.
65
+ *
66
+ * @param agents - Record of agent name to AgentDefinition
67
+ * @param defaults - Optional default values for provider/model/temperature
68
+ * @returns Object with all helper functions
69
+ *
70
+ * @example
71
+ * ```typescript
72
+ * const AGENTS = {
73
+ * 'single-agent': {
74
+ * provider: 'ollama',
75
+ * model: 'llama3.2:3b',
76
+ * temperature: 0.3,
77
+ * systemPrompt: 'single-agent',
78
+ * createTools: (ctx) => [...tools],
79
+ * },
80
+ * }
81
+ *
82
+ * const helpers = createAgentHelpers(AGENTS)
83
+ * const config = helpers.getAgentConfig('single-agent')
84
+ * const tools = helpers.getAgentTools('single-agent', { userId, teamId })
85
+ * ```
86
+ */
87
+ export function createAgentHelpers(
88
+ agents: Record<string, AgentDefinition>,
89
+ defaults?: {
90
+ provider?: 'openai' | 'anthropic' | 'ollama'
91
+ model?: string
92
+ temperature?: number
93
+ }
94
+ ): AgentHelpers {
95
+ return {
96
+ /**
97
+ * Get complete configuration for an agent
98
+ */
99
+ getAgentConfig(agentName: string): AgentDefinition | undefined {
100
+ return agents[agentName]
101
+ },
102
+
103
+ /**
104
+ * Get model configuration for an agent (provider, model, temperature)
105
+ */
106
+ getAgentModelConfig(agentName: string): Partial<{
107
+ provider: 'openai' | 'anthropic' | 'ollama'
108
+ model: string
109
+ temperature: number
110
+ }> | undefined {
111
+ const agent = agents[agentName]
112
+ if (!agent) {
113
+ // Fall back to defaults if provided
114
+ if (defaults?.provider) {
115
+ return {
116
+ provider: defaults.provider,
117
+ model: defaults.model,
118
+ temperature: defaults.temperature,
119
+ }
120
+ }
121
+ return undefined
122
+ }
123
+
124
+ return {
125
+ provider: agent.provider,
126
+ model: agent.model,
127
+ temperature: agent.temperature,
128
+ }
129
+ },
130
+
131
+ /**
132
+ * Get tools for an agent with runtime context
133
+ */
134
+ getAgentTools(
135
+ agentName: string,
136
+ context: AgentContext
137
+ ): ToolDefinition<any>[] {
138
+ const agent = agents[agentName]
139
+ if (!agent?.createTools) {
140
+ return []
141
+ }
142
+ return agent.createTools(context) as ToolDefinition<any>[]
143
+ },
144
+
145
+ /**
146
+ * Get system prompt name for an agent
147
+ * Returns the prompt name to be loaded from .md file
148
+ */
149
+ getAgentPromptName(agentName: string): string | undefined {
150
+ const agent = agents[agentName]
151
+ return agent?.systemPrompt
152
+ },
153
+
154
+ /**
155
+ * Get session configuration for an agent (TTL, maxMessages)
156
+ */
157
+ getAgentSessionConfig(agentName: string): SessionConfig | undefined {
158
+ const agent = agents[agentName]
159
+ return agent?.sessionConfig
160
+ },
161
+
162
+ /**
163
+ * Get enrichContext function for an agent
164
+ */
165
+ getAgentEnrichContext(
166
+ agentName: string
167
+ ): ((context: AgentContext) => Promise<AgentContext>) | undefined {
168
+ const agent = agents[agentName]
169
+ return agent?.enrichContext
170
+ },
171
+
172
+ /**
173
+ * Check if an agent is configured
174
+ */
175
+ hasAgent(agentName: string): boolean {
176
+ return agentName in agents
177
+ },
178
+
179
+ /**
180
+ * Get all configured agent names
181
+ */
182
+ getAgentNames(): string[] {
183
+ return Object.keys(agents)
184
+ },
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Create helpers from a full ThemeLangChainConfig
190
+ * Convenience function that extracts agents and defaults
191
+ */
192
+ export function createHelpersFromConfig(config: ThemeLangChainConfig): AgentHelpers {
193
+ return createAgentHelpers(
194
+ config.agents || {},
195
+ {
196
+ provider: config.defaultProvider,
197
+ model: config.defaultModel,
198
+ temperature: config.defaultTemperature,
199
+ }
200
+ )
201
+ }
@@ -0,0 +1,417 @@
1
+ /**
2
+ * Database Memory Store for LangChain
3
+ *
4
+ * Persistent storage for conversation history using PostgreSQL.
5
+ * Supports multi-tenancy with userId + teamId + sessionId isolation.
6
+ * Supports multiple conversations per user with no expiration by default.
7
+ */
8
+
9
+ import { BaseMessage, HumanMessage } from '@langchain/core/messages'
10
+ import { queryWithRLS, mutateWithRLS, query } from '@nextsparkjs/core/lib/db'
11
+ import {
12
+ serializeMessages,
13
+ deserializeMessages,
14
+ type SerializedMessage,
15
+ } from './message-serializer'
16
+ import type { AgentContext, SessionConfig } from '../types/langchain.types'
17
+
18
+ /**
19
+ * Conversation/Session information returned by list and get operations
20
+ */
21
+ export interface ConversationInfo {
22
+ sessionId: string
23
+ name: string | null
24
+ messageCount: number
25
+ firstMessage: string | null
26
+ isPinned: boolean
27
+ createdAt: Date
28
+ updatedAt: Date
29
+ }
30
+
31
+ /**
32
+ * Conversation limits
33
+ */
34
+ export const CONVERSATION_LIMITS = {
35
+ MAX_CONVERSATIONS: 50,
36
+ MAX_MESSAGES_PER_CONVERSATION: 50,
37
+ } as const
38
+
39
+ const DEFAULT_MAX_MESSAGES = CONVERSATION_LIMITS.MAX_MESSAGES_PER_CONVERSATION
40
+ const DEFAULT_TTL_HOURS: number | null = null // No expiration by default
41
+
42
+ /**
43
+ * Row type from database
44
+ */
45
+ interface SessionRow {
46
+ id: string
47
+ userId: string
48
+ teamId: string
49
+ sessionId: string
50
+ name: string | null
51
+ isPinned: boolean
52
+ messages: SerializedMessage[]
53
+ maxMessages: number
54
+ expiresAt: Date | null
55
+ createdAt: Date
56
+ updatedAt: Date
57
+ }
58
+
59
+ /**
60
+ * Generate a new session ID
61
+ */
62
+ export function generateSessionId(userId: string): string {
63
+ return `${userId}-${Date.now()}`
64
+ }
65
+
66
+ /**
67
+ * Extract first human message content from serialized messages
68
+ */
69
+ function getFirstHumanMessage(messages: SerializedMessage[]): string | null {
70
+ const humanMessage = messages.find((m) => m.type === 'human')
71
+ if (!humanMessage) return null
72
+
73
+ const content = humanMessage.content
74
+ if (typeof content === 'string') {
75
+ return content.slice(0, 100) // Truncate to 100 chars
76
+ }
77
+ return null
78
+ }
79
+
80
+ /**
81
+ * Database-backed memory store with full multi-tenancy support
82
+ */
83
+ export const dbMemoryStore = {
84
+ /**
85
+ * Get messages for a session
86
+ */
87
+ getMessages: async (
88
+ sessionId: string,
89
+ context: AgentContext
90
+ ): Promise<BaseMessage[]> => {
91
+ const { userId, teamId } = context
92
+
93
+ const result = await queryWithRLS<SessionRow>(
94
+ `SELECT messages
95
+ FROM public."langchain_sessions"
96
+ WHERE "userId" = $1 AND "teamId" = $2 AND "sessionId" = $3
97
+ AND ("expiresAt" IS NULL OR "expiresAt" > now())`,
98
+ [userId, teamId, sessionId],
99
+ userId
100
+ )
101
+
102
+ if (!result.length) return []
103
+
104
+ return deserializeMessages(result[0].messages)
105
+ },
106
+
107
+ /**
108
+ * Get full session info (not just messages)
109
+ */
110
+ getSession: async (
111
+ sessionId: string,
112
+ context: AgentContext
113
+ ): Promise<ConversationInfo | null> => {
114
+ const { userId, teamId } = context
115
+
116
+ const result = await queryWithRLS<SessionRow>(
117
+ `SELECT "sessionId", name, "isPinned", messages, "createdAt", "updatedAt"
118
+ FROM public."langchain_sessions"
119
+ WHERE "userId" = $1 AND "teamId" = $2 AND "sessionId" = $3
120
+ AND ("expiresAt" IS NULL OR "expiresAt" > now())`,
121
+ [userId, teamId, sessionId],
122
+ userId
123
+ )
124
+
125
+ if (!result.length) return null
126
+
127
+ const row = result[0]
128
+ return {
129
+ sessionId: row.sessionId,
130
+ name: row.name,
131
+ messageCount: row.messages.length,
132
+ firstMessage: getFirstHumanMessage(row.messages),
133
+ isPinned: row.isPinned,
134
+ createdAt: row.createdAt,
135
+ updatedAt: row.updatedAt,
136
+ }
137
+ },
138
+
139
+ /**
140
+ * Add messages to a session (upsert with sliding window)
141
+ * Auto-generates name from first message if not set
142
+ *
143
+ * OPTIMIZED: Uses a single query with CTE instead of 3 separate queries
144
+ * - Reads existing messages, combines with new, applies sliding window
145
+ * - All done atomically in PostgreSQL
146
+ */
147
+ addMessages: async (
148
+ sessionId: string,
149
+ messages: BaseMessage[],
150
+ context: AgentContext,
151
+ config: SessionConfig = {}
152
+ ): Promise<void> => {
153
+ const { userId, teamId } = context
154
+ const maxMessages = config.maxMessages || DEFAULT_MAX_MESSAGES
155
+ const ttlHours = config.ttlHours === undefined ? DEFAULT_TTL_HOURS : config.ttlHours
156
+
157
+ // Serialize new messages
158
+ const newMessagesSerialized = serializeMessages(messages)
159
+
160
+ // Auto-generate name from first human message
161
+ let autoName: string | null = null
162
+ if (messages.length > 0) {
163
+ const firstHuman = messages.find((m) => m instanceof HumanMessage)
164
+ if (firstHuman) {
165
+ const content = firstHuman.content
166
+ if (typeof content === 'string') {
167
+ autoName = content.slice(0, 50) // First 50 chars
168
+ }
169
+ }
170
+ }
171
+
172
+ // Calculate expiresAt
173
+ const expiresAt = ttlHours !== null
174
+ ? new Date(Date.now() + ttlHours * 60 * 60 * 1000)
175
+ : null
176
+
177
+ // Single atomic query: read existing + combine + sliding window + upsert
178
+ // Uses CTE to:
179
+ // 1. Get existing messages (or empty array)
180
+ // 2. Combine with new messages
181
+ // 3. Apply sliding window (keep last N messages)
182
+ // 4. Upsert the result
183
+ await mutateWithRLS(
184
+ `WITH existing_session AS (
185
+ SELECT messages, name
186
+ FROM public."langchain_sessions"
187
+ WHERE "userId" = $1 AND "teamId" = $2 AND "sessionId" = $3
188
+ ),
189
+ combined AS (
190
+ SELECT COALESCE((SELECT messages FROM existing_session), '[]'::jsonb) || $4::jsonb as all_msgs
191
+ ),
192
+ windowed AS (
193
+ SELECT COALESCE(
194
+ (
195
+ SELECT jsonb_agg(value ORDER BY ordinality)
196
+ FROM (
197
+ SELECT value, ordinality
198
+ FROM combined, jsonb_array_elements(all_msgs) WITH ORDINALITY
199
+ ) sub
200
+ WHERE ordinality > jsonb_array_length((SELECT all_msgs FROM combined)) - $5
201
+ ),
202
+ $4::jsonb
203
+ ) as final_msgs
204
+ )
205
+ INSERT INTO public."langchain_sessions"
206
+ (id, "userId", "teamId", "sessionId", messages, "maxMessages", "expiresAt", name)
207
+ SELECT
208
+ gen_random_uuid()::text, $1, $2, $3,
209
+ (SELECT final_msgs FROM windowed),
210
+ $5, $6,
211
+ COALESCE((SELECT name FROM existing_session), $7)
212
+ ON CONFLICT ("userId", "teamId", "sessionId")
213
+ DO UPDATE SET
214
+ messages = EXCLUDED.messages,
215
+ "expiresAt" = COALESCE(public."langchain_sessions"."expiresAt", EXCLUDED."expiresAt"),
216
+ name = COALESCE(public."langchain_sessions".name, EXCLUDED.name),
217
+ "updatedAt" = now()`,
218
+ [userId, teamId, sessionId, JSON.stringify(newMessagesSerialized), maxMessages, expiresAt, autoName],
219
+ userId
220
+ )
221
+ },
222
+
223
+ /**
224
+ * Create a new empty session/conversation
225
+ */
226
+ createSession: async (
227
+ context: AgentContext,
228
+ name?: string
229
+ ): Promise<{ sessionId: string; createdAt: Date }> => {
230
+ const { userId, teamId } = context
231
+ const sessionId = generateSessionId(userId)
232
+
233
+ const result = await queryWithRLS<{ sessionId: string; createdAt: Date }>(
234
+ `INSERT INTO public."langchain_sessions"
235
+ (id, "userId", "teamId", "sessionId", messages, name)
236
+ VALUES (gen_random_uuid()::text, $1, $2, $3, '[]'::jsonb, $4)
237
+ RETURNING "sessionId", "createdAt"`,
238
+ [userId, teamId, sessionId, name || null],
239
+ userId
240
+ )
241
+
242
+ return result[0]
243
+ },
244
+
245
+ /**
246
+ * Rename a session
247
+ */
248
+ renameSession: async (
249
+ sessionId: string,
250
+ name: string,
251
+ context: AgentContext
252
+ ): Promise<void> => {
253
+ const { userId, teamId } = context
254
+
255
+ await mutateWithRLS(
256
+ `UPDATE public."langchain_sessions"
257
+ SET name = $4, "updatedAt" = now()
258
+ WHERE "userId" = $1 AND "teamId" = $2 AND "sessionId" = $3`,
259
+ [userId, teamId, sessionId, name],
260
+ userId
261
+ )
262
+ },
263
+
264
+ /**
265
+ * Toggle pin status of a session
266
+ */
267
+ togglePinSession: async (
268
+ sessionId: string,
269
+ isPinned: boolean,
270
+ context: AgentContext
271
+ ): Promise<void> => {
272
+ const { userId, teamId } = context
273
+
274
+ await mutateWithRLS(
275
+ `UPDATE public."langchain_sessions"
276
+ SET "isPinned" = $4, "updatedAt" = now()
277
+ WHERE "userId" = $1 AND "teamId" = $2 AND "sessionId" = $3`,
278
+ [userId, teamId, sessionId, isPinned],
279
+ userId
280
+ )
281
+ },
282
+
283
+ /**
284
+ * Count sessions for a user (for limit enforcement)
285
+ */
286
+ countSessions: async (context: AgentContext): Promise<number> => {
287
+ const { userId, teamId } = context
288
+
289
+ const result = await queryWithRLS<{ count: string }>(
290
+ `SELECT COUNT(*)::text as count
291
+ FROM public."langchain_sessions"
292
+ WHERE "userId" = $1 AND "teamId" = $2
293
+ AND ("expiresAt" IS NULL OR "expiresAt" > now())`,
294
+ [userId, teamId],
295
+ userId
296
+ )
297
+
298
+ return parseInt(result[0]?.count || '0', 10)
299
+ },
300
+
301
+ /**
302
+ * Get oldest session (for suggesting deletion when limit reached)
303
+ */
304
+ getOldestSession: async (
305
+ context: AgentContext
306
+ ): Promise<ConversationInfo | null> => {
307
+ const { userId, teamId } = context
308
+
309
+ const result = await queryWithRLS<SessionRow>(
310
+ `SELECT "sessionId", name, "isPinned", messages, "createdAt", "updatedAt"
311
+ FROM public."langchain_sessions"
312
+ WHERE "userId" = $1 AND "teamId" = $2
313
+ AND ("expiresAt" IS NULL OR "expiresAt" > now())
314
+ AND "isPinned" = false
315
+ ORDER BY "updatedAt" ASC
316
+ LIMIT 1`,
317
+ [userId, teamId],
318
+ userId
319
+ )
320
+
321
+ if (!result.length) return null
322
+
323
+ const row = result[0]
324
+ return {
325
+ sessionId: row.sessionId,
326
+ name: row.name,
327
+ messageCount: row.messages.length,
328
+ firstMessage: getFirstHumanMessage(row.messages),
329
+ isPinned: row.isPinned,
330
+ createdAt: row.createdAt,
331
+ updatedAt: row.updatedAt,
332
+ }
333
+ },
334
+
335
+ /**
336
+ * Clear/delete a session
337
+ */
338
+ clearSession: async (
339
+ sessionId: string,
340
+ context: AgentContext
341
+ ): Promise<void> => {
342
+ const { userId, teamId } = context
343
+
344
+ await mutateWithRLS(
345
+ `DELETE FROM public."langchain_sessions"
346
+ WHERE "userId" = $1 AND "teamId" = $2 AND "sessionId" = $3`,
347
+ [userId, teamId, sessionId],
348
+ userId
349
+ )
350
+ },
351
+
352
+ /**
353
+ * Clean up expired sessions (run periodically)
354
+ * Only cleans sessions with expiresAt set and expired.
355
+ * Sessions with expiresAt = NULL are never cleaned up.
356
+ */
357
+ cleanup: async (): Promise<number> => {
358
+ const result = await query<{ id: string }>(
359
+ `DELETE FROM public."langchain_sessions"
360
+ WHERE "expiresAt" IS NOT NULL AND "expiresAt" < now()
361
+ RETURNING id`
362
+ )
363
+
364
+ return result.rowCount
365
+ },
366
+
367
+ /**
368
+ * Get all sessions for a user in a team
369
+ * Returns sessions sorted by: pinned first, then by updatedAt desc
370
+ */
371
+ listSessions: async (
372
+ context: AgentContext
373
+ ): Promise<ConversationInfo[]> => {
374
+ const { userId, teamId } = context
375
+
376
+ const result = await queryWithRLS<SessionRow>(
377
+ `SELECT "sessionId", name, "isPinned", messages, "createdAt", "updatedAt"
378
+ FROM public."langchain_sessions"
379
+ WHERE "userId" = $1 AND "teamId" = $2
380
+ AND ("expiresAt" IS NULL OR "expiresAt" > now())
381
+ ORDER BY "isPinned" DESC, "updatedAt" DESC`,
382
+ [userId, teamId],
383
+ userId
384
+ )
385
+
386
+ return result.map((row) => ({
387
+ sessionId: row.sessionId,
388
+ name: row.name,
389
+ messageCount: row.messages.length,
390
+ firstMessage: getFirstHumanMessage(row.messages),
391
+ isPinned: row.isPinned,
392
+ createdAt: row.createdAt,
393
+ updatedAt: row.updatedAt,
394
+ }))
395
+ },
396
+
397
+ /**
398
+ * Extend session TTL (deprecated - sessions now have no expiration by default)
399
+ * @deprecated Use for legacy support only
400
+ */
401
+ extendSession: async (
402
+ sessionId: string,
403
+ context: AgentContext,
404
+ ttlHours: number = 24
405
+ ): Promise<void> => {
406
+ const { userId, teamId } = context
407
+ const expiresAt = new Date(Date.now() + ttlHours * 60 * 60 * 1000)
408
+
409
+ await mutateWithRLS(
410
+ `UPDATE public."langchain_sessions"
411
+ SET "expiresAt" = $4, "updatedAt" = now()
412
+ WHERE "userId" = $1 AND "teamId" = $2 AND "sessionId" = $3`,
413
+ [userId, teamId, sessionId, expiresAt],
414
+ userId
415
+ )
416
+ },
417
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * LangGraph Orchestrator - Main Export (GENERIC)
3
+ *
4
+ * Explicit state machine for multi-agent orchestration.
5
+ * Replaces inefficient ReAct loops with deterministic graph flow.
6
+ *
7
+ * GENERIC ARCHITECTURE:
8
+ * - Plugin exports generic interfaces (AgentTool, OrchestratorConfig)
9
+ * - Theme provides tool implementations and configuration
10
+ * - No hardcoded entity knowledge in plugin
11
+ */
12
+
13
+ // Generic types (NEW)
14
+ export type {
15
+ AgentTool,
16
+ OrchestratorConfig,
17
+ GenericHandlerResult,
18
+ } from './types'
19
+
20
+ // Core types
21
+ export type {
22
+ OrchestratorState,
23
+ Intent,
24
+ IntentType,
25
+ IntentAction,
26
+ RouterOutput,
27
+ HandlerResults,
28
+ GraphConfig,
29
+ RouterRoute,
30
+ PostHandlerRoute,
31
+ HandlerNodeFn,
32
+ ModelConfig,
33
+ SystemIntentType,
34
+ } from './types'
35
+
36
+ // Deprecated types (backward compatibility)
37
+ export type {
38
+ TaskHandlerResult,
39
+ CustomerHandlerResult,
40
+ PageHandlerResult,
41
+ TaskData,
42
+ CustomerData,
43
+ PageData,
44
+ HandlerFactories,
45
+ } from './types'
46
+
47
+ export { createInitialState, DEFAULT_GRAPH_CONFIG } from './types'
48
+
49
+ // Graph
50
+ export { createOrchestratorGraph, invokeOrchestrator } from './orchestrator-graph'
51
+
52
+ // Router node factory (NEW - replaces direct routerNode export)
53
+ export { createRouterNode } from './nodes/router'
54
+
55
+ // Core nodes
56
+ export { combinerNode } from './nodes/combiner'
57
+
58
+ // Handler nodes are theme-specific - themes must provide implementations via OrchestratorConfig