@node-llm/orm 0.3.0 → 0.5.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/CHANGELOG.md +71 -2
- package/README.md +94 -8
- package/bin/cli.js +40 -6
- package/dist/BaseChat.d.ts +3 -1
- package/dist/BaseChat.d.ts.map +1 -1
- package/dist/BaseChat.js +5 -0
- package/dist/adapters/prisma/AgentSession.d.ts +140 -0
- package/dist/adapters/prisma/AgentSession.d.ts.map +1 -0
- package/dist/adapters/prisma/AgentSession.js +284 -0
- package/dist/adapters/prisma/Chat.d.ts +3 -2
- package/dist/adapters/prisma/Chat.d.ts.map +1 -1
- package/dist/adapters/prisma/Chat.js +34 -4
- package/dist/adapters/prisma/index.d.ts +25 -2
- package/dist/adapters/prisma/index.d.ts.map +1 -1
- package/dist/adapters/prisma/index.js +25 -1
- package/dist/index.d.ts +21 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -1
- package/migrations/README.md +53 -0
- package/migrations/add_agent_session.sql +44 -0
- package/migrations/add_thinking_support.sql +34 -0
- package/package.json +6 -2
- package/schema.prisma +50 -33
- package/src/BaseChat.ts +8 -1
- package/src/adapters/prisma/AgentSession.ts +458 -0
- package/src/adapters/prisma/Chat.ts +55 -7
- package/src/adapters/prisma/index.ts +33 -2
- package/src/index.ts +21 -1
- package/test/AgentSession.test.ts +204 -0
- package/test/CodeWins.test.ts +116 -0
- package/test/Middleware.test.ts +137 -0
- package/test/Strictness.test.ts +117 -0
- package/test/docs/prisma-docs.test.ts +221 -0
- package/test/docs/readme-exports.test.ts +62 -0
- package/tsconfig.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@node-llm/orm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Database persistence layer for NodeLLM - Chat, Message, and ToolCall tracking with streaming support",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
5
8
|
"type": "module",
|
|
6
9
|
"main": "./dist/index.js",
|
|
7
10
|
"types": "./dist/index.d.ts",
|
|
@@ -53,7 +56,7 @@
|
|
|
53
56
|
"author": "NodeLLM Contributors",
|
|
54
57
|
"license": "MIT",
|
|
55
58
|
"peerDependencies": {
|
|
56
|
-
"@node-llm/core": "^1.
|
|
59
|
+
"@node-llm/core": "^1.10.0",
|
|
57
60
|
"@prisma/client": "^5.0.0"
|
|
58
61
|
},
|
|
59
62
|
"devDependencies": {
|
|
@@ -68,6 +71,7 @@
|
|
|
68
71
|
"scripts": {
|
|
69
72
|
"build": "prisma generate --schema=schema.prisma && tsc",
|
|
70
73
|
"test": "vitest run",
|
|
74
|
+
"test:docs": "vitest run test/docs",
|
|
71
75
|
"test:watch": "vitest"
|
|
72
76
|
}
|
|
73
77
|
}
|
package/schema.prisma
CHANGED
|
@@ -12,53 +12,70 @@ datasource db {
|
|
|
12
12
|
|
|
13
13
|
// NodeLLM ORM Models (matches @node-llm/orm schema)
|
|
14
14
|
model LlmChat {
|
|
15
|
-
id String
|
|
15
|
+
id String @id @default(uuid())
|
|
16
16
|
model String?
|
|
17
17
|
provider String?
|
|
18
|
-
instructions String?
|
|
19
|
-
metadata Json?
|
|
20
|
-
createdAt DateTime
|
|
21
|
-
updatedAt DateTime
|
|
18
|
+
instructions String? // System instructions
|
|
19
|
+
metadata Json? // JSON metadata
|
|
20
|
+
createdAt DateTime @default(now())
|
|
21
|
+
updatedAt DateTime @updatedAt
|
|
22
22
|
messages LlmMessage[]
|
|
23
23
|
requests LlmRequest[]
|
|
24
|
+
agentSession LlmAgentSession?
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Agent Session - Links Agent class to persistent Chat
|
|
28
|
+
// "Code defines behavior, DB provides history"
|
|
29
|
+
model LlmAgentSession {
|
|
30
|
+
id String @id @default(uuid())
|
|
31
|
+
agentClass String // Class name for validation (e.g., 'SupportAgent')
|
|
32
|
+
chatId String @unique
|
|
33
|
+
metadata Json? // Session context (userId, ticketId, etc.)
|
|
34
|
+
createdAt DateTime @default(now())
|
|
35
|
+
updatedAt DateTime @updatedAt
|
|
36
|
+
|
|
37
|
+
chat LlmChat @relation(fields: [chatId], references: [id], onDelete: Cascade)
|
|
38
|
+
|
|
39
|
+
@@index([agentClass])
|
|
40
|
+
@@index([createdAt])
|
|
24
41
|
}
|
|
25
42
|
|
|
26
43
|
model LlmMessage {
|
|
27
|
-
id String
|
|
44
|
+
id String @id @default(uuid())
|
|
28
45
|
chatId String
|
|
29
|
-
role String
|
|
46
|
+
role String // user, assistant, system, tool
|
|
30
47
|
content String?
|
|
31
|
-
contentRaw String?
|
|
32
|
-
reasoning String?
|
|
33
|
-
thinkingText String?
|
|
34
|
-
thinkingSignature String?
|
|
35
|
-
thinkingTokens Int?
|
|
48
|
+
contentRaw String? // JSON raw payload
|
|
49
|
+
reasoning String? // Chain of thought (deprecated)
|
|
50
|
+
thinkingText String? // Extended thinking text
|
|
51
|
+
thinkingSignature String? // Cryptographic signature
|
|
52
|
+
thinkingTokens Int? // Tokens spent on thinking
|
|
36
53
|
inputTokens Int?
|
|
37
54
|
outputTokens Int?
|
|
38
55
|
modelId String?
|
|
39
56
|
provider String?
|
|
40
|
-
createdAt DateTime
|
|
57
|
+
createdAt DateTime @default(now())
|
|
41
58
|
|
|
42
|
-
chat
|
|
43
|
-
toolCalls
|
|
44
|
-
requests
|
|
59
|
+
chat LlmChat @relation(fields: [chatId], references: [id], onDelete: Cascade)
|
|
60
|
+
toolCalls LlmToolCall[]
|
|
61
|
+
requests LlmRequest[]
|
|
45
62
|
|
|
46
63
|
@@index([chatId])
|
|
47
64
|
@@index([createdAt])
|
|
48
65
|
}
|
|
49
66
|
|
|
50
67
|
model LlmToolCall {
|
|
51
|
-
id String
|
|
68
|
+
id String @id @default(uuid())
|
|
52
69
|
messageId String
|
|
53
|
-
toolCallId String
|
|
70
|
+
toolCallId String // ID from the provider
|
|
54
71
|
name String
|
|
55
|
-
arguments String
|
|
56
|
-
thought String?
|
|
57
|
-
thoughtSignature String?
|
|
58
|
-
result String?
|
|
59
|
-
createdAt DateTime
|
|
72
|
+
arguments String // JSON string
|
|
73
|
+
thought String? // The LLM's reasoning for this tool call
|
|
74
|
+
thoughtSignature String? // Signature for the thought
|
|
75
|
+
result String? // Tool execution result
|
|
76
|
+
createdAt DateTime @default(now())
|
|
60
77
|
|
|
61
|
-
message
|
|
78
|
+
message LlmMessage @relation(fields: [messageId], references: [id], onDelete: Cascade)
|
|
62
79
|
|
|
63
80
|
@@unique([messageId, toolCallId])
|
|
64
81
|
@@index([messageId])
|
|
@@ -66,22 +83,22 @@ model LlmToolCall {
|
|
|
66
83
|
}
|
|
67
84
|
|
|
68
85
|
model LlmRequest {
|
|
69
|
-
id
|
|
70
|
-
chatId
|
|
71
|
-
messageId
|
|
72
|
-
|
|
86
|
+
id String @id @default(uuid())
|
|
87
|
+
chatId String
|
|
88
|
+
messageId String? // Optional because requests might fail before message creation
|
|
89
|
+
|
|
73
90
|
provider String
|
|
74
91
|
model String
|
|
75
92
|
statusCode Int
|
|
76
|
-
duration Int
|
|
93
|
+
duration Int // milliseconds
|
|
77
94
|
inputTokens Int
|
|
78
95
|
outputTokens Int
|
|
79
96
|
cost Float?
|
|
80
|
-
|
|
81
|
-
createdAt DateTime @default(now())
|
|
82
97
|
|
|
83
|
-
|
|
84
|
-
|
|
98
|
+
createdAt DateTime @default(now())
|
|
99
|
+
|
|
100
|
+
chat LlmChat @relation(fields: [chatId], references: [id], onDelete: Cascade)
|
|
101
|
+
message LlmMessage? @relation(fields: [messageId], references: [id], onDelete: Cascade)
|
|
85
102
|
|
|
86
103
|
@@index([chatId])
|
|
87
104
|
@@index([createdAt])
|
package/src/BaseChat.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import type { Usage } from "@node-llm/core";
|
|
2
|
+
import type { Usage, Middleware } from "@node-llm/core";
|
|
3
3
|
|
|
4
4
|
export interface ChatRecord {
|
|
5
5
|
id: string;
|
|
@@ -31,6 +31,7 @@ export interface ChatOptions {
|
|
|
31
31
|
maxToolCalls?: number;
|
|
32
32
|
requestTimeout?: number;
|
|
33
33
|
params?: Record<string, any>;
|
|
34
|
+
middlewares?: Middleware[];
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
export interface UserHooks {
|
|
@@ -52,6 +53,7 @@ export abstract class BaseChat<
|
|
|
52
53
|
public id: string;
|
|
53
54
|
protected localOptions: any = {};
|
|
54
55
|
protected customTools: any[] = [];
|
|
56
|
+
protected customMiddlewares: Middleware[] = [];
|
|
55
57
|
protected userHooks: UserHooks = {
|
|
56
58
|
onToolCallStart: [],
|
|
57
59
|
onToolCallEnd: [],
|
|
@@ -78,6 +80,11 @@ export abstract class BaseChat<
|
|
|
78
80
|
this.localOptions.maxToolCalls = options.maxToolCalls;
|
|
79
81
|
this.localOptions.requestTimeout = options.requestTimeout;
|
|
80
82
|
this.localOptions.params = options.params;
|
|
83
|
+
|
|
84
|
+
// Initialize middlewares
|
|
85
|
+
if (options.middlewares) {
|
|
86
|
+
this.customMiddlewares = options.middlewares;
|
|
87
|
+
}
|
|
81
88
|
}
|
|
82
89
|
|
|
83
90
|
protected log(...args: any[]) {
|
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChatOptions,
|
|
3
|
+
AskOptions,
|
|
4
|
+
NodeLLMCore,
|
|
5
|
+
Agent,
|
|
6
|
+
AgentConfig,
|
|
7
|
+
Message,
|
|
8
|
+
ChatChunk,
|
|
9
|
+
Usage
|
|
10
|
+
} from "@node-llm/core";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Internal interface for dynamic Prisma Client access.
|
|
14
|
+
* We use 'any' here because PrismaClient has no index signature by default,
|
|
15
|
+
* making it hard to access models dynamically by string name.
|
|
16
|
+
*/
|
|
17
|
+
type GenericPrismaClient = any;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Record structure for the LLM Agent Session table.
|
|
21
|
+
*/
|
|
22
|
+
export interface AgentSessionRecord {
|
|
23
|
+
id: string;
|
|
24
|
+
agentClass: string;
|
|
25
|
+
chatId: string;
|
|
26
|
+
metadata?: Record<string, unknown> | null;
|
|
27
|
+
createdAt: Date;
|
|
28
|
+
updatedAt: Date;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Record structure for the LLM Message table.
|
|
33
|
+
*/
|
|
34
|
+
export interface MessageRecord {
|
|
35
|
+
id: string;
|
|
36
|
+
chatId: string;
|
|
37
|
+
role: string;
|
|
38
|
+
content: string | null;
|
|
39
|
+
contentRaw?: string | null;
|
|
40
|
+
thinkingText?: string | null;
|
|
41
|
+
thinkingSignature?: string | null;
|
|
42
|
+
thinkingTokens?: number | null;
|
|
43
|
+
inputTokens?: number | null;
|
|
44
|
+
outputTokens?: number | null;
|
|
45
|
+
modelId?: string | null;
|
|
46
|
+
provider?: string | null;
|
|
47
|
+
createdAt: Date;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Table name customization.
|
|
52
|
+
*/
|
|
53
|
+
export interface TableNames {
|
|
54
|
+
agentSession?: string;
|
|
55
|
+
chat?: string;
|
|
56
|
+
message?: string;
|
|
57
|
+
toolCall?: string;
|
|
58
|
+
request?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Internal interface for dynamic Prisma model access
|
|
63
|
+
*/
|
|
64
|
+
interface PrismaModel<T = Record<string, unknown>> {
|
|
65
|
+
create(args: { data: Record<string, unknown> }): Promise<T>;
|
|
66
|
+
update(args: { where: { id: string }; data: Record<string, unknown> }): Promise<T>;
|
|
67
|
+
delete(args: { where: { id: string } }): Promise<void>;
|
|
68
|
+
findMany(args: {
|
|
69
|
+
where: Record<string, unknown>;
|
|
70
|
+
orderBy?: Record<string, string>;
|
|
71
|
+
}): Promise<T[]>;
|
|
72
|
+
findUnique(args: { where: { id: string } }): Promise<T | null>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type AgentClass<T extends Agent = Agent> = (new (
|
|
76
|
+
overrides?: Partial<AgentConfig & ChatOptions>
|
|
77
|
+
) => T) & {
|
|
78
|
+
name: string;
|
|
79
|
+
model?: string;
|
|
80
|
+
instructions?: string;
|
|
81
|
+
tools?: unknown[];
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* AgentSession - Wraps an Agent instance with persistence capabilities.
|
|
86
|
+
*
|
|
87
|
+
* Follows "Code Wins" sovereignty:
|
|
88
|
+
* - Model, Tools, Instructions come from the Agent class (code)
|
|
89
|
+
* - Message history comes from the database
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```typescript
|
|
93
|
+
* // Create a new session
|
|
94
|
+
* const session = await createAgentSession(prisma, llm, SupportAgent, {
|
|
95
|
+
* metadata: { userId: "123" }
|
|
96
|
+
* });
|
|
97
|
+
*
|
|
98
|
+
* // Resume a session
|
|
99
|
+
* const session = await loadAgentSession(prisma, llm, SupportAgent, "sess_abc");
|
|
100
|
+
*
|
|
101
|
+
* // Agent behavior is always defined in code
|
|
102
|
+
* const result = await session.ask("Hello");
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
export class AgentSession<T extends Agent = Agent> {
|
|
106
|
+
private currentMessageId: string | null = null;
|
|
107
|
+
private tableNames: Required<TableNames>;
|
|
108
|
+
private debug: boolean;
|
|
109
|
+
|
|
110
|
+
constructor(
|
|
111
|
+
private prisma: any,
|
|
112
|
+
private llm: NodeLLMCore,
|
|
113
|
+
private AgentClass: AgentClass<T>,
|
|
114
|
+
private record: AgentSessionRecord,
|
|
115
|
+
tableNames?: TableNames,
|
|
116
|
+
private agent: T = new AgentClass({
|
|
117
|
+
llm
|
|
118
|
+
}),
|
|
119
|
+
debug: boolean = false
|
|
120
|
+
) {
|
|
121
|
+
this.debug = debug;
|
|
122
|
+
this.tableNames = {
|
|
123
|
+
agentSession: tableNames?.agentSession || "llmAgentSession",
|
|
124
|
+
chat: tableNames?.chat || "llmChat",
|
|
125
|
+
message: tableNames?.message || "llmMessage",
|
|
126
|
+
toolCall: tableNames?.toolCall || "llmToolCall",
|
|
127
|
+
request: tableNames?.request || "llmRequest"
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private log(...args: any[]) {
|
|
132
|
+
if (this.debug) {
|
|
133
|
+
console.log(`[@node-llm/orm]`, ...args);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Agent instance (for direct access if needed) */
|
|
138
|
+
get instance(): T {
|
|
139
|
+
return this.agent;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Session ID for persistence */
|
|
143
|
+
get id(): string {
|
|
144
|
+
return this.record.id;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Underlying chat ID */
|
|
148
|
+
get chatId(): string {
|
|
149
|
+
return this.record.chatId;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Session metadata */
|
|
153
|
+
get metadata(): Record<string, unknown> | null | undefined {
|
|
154
|
+
return this.record.metadata;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Agent class name */
|
|
158
|
+
get agentClass(): string {
|
|
159
|
+
return this.record.agentClass;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Model ID used by the agent */
|
|
163
|
+
get modelId(): string {
|
|
164
|
+
return this.agent.modelId;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Cumulative usage for this session (from agent memory) */
|
|
168
|
+
get totalUsage(): Usage {
|
|
169
|
+
return this.agent.totalUsage;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Current in-memory message history */
|
|
173
|
+
get history(): readonly Message[] {
|
|
174
|
+
return this.agent.history;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Helper to get a typed Prisma model by its dynamic name.
|
|
179
|
+
*/
|
|
180
|
+
private getModel<R = Record<string, unknown>>(name: string): PrismaModel<R> {
|
|
181
|
+
return getTable(this.prisma, name) as unknown as PrismaModel<R>;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Send a message and persist the conversation.
|
|
186
|
+
*/
|
|
187
|
+
async ask(input: string, options: AskOptions = {}): Promise<MessageRecord> {
|
|
188
|
+
const model = this.getModel<MessageRecord>(this.tableNames.message);
|
|
189
|
+
|
|
190
|
+
// Persist user message
|
|
191
|
+
await model.create({
|
|
192
|
+
data: { chatId: this.chatId, role: "user", content: input }
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Create placeholder for assistant message
|
|
196
|
+
const assistantMessage = await model.create({
|
|
197
|
+
data: { chatId: this.chatId, role: "assistant", content: null }
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
this.currentMessageId = assistantMessage.id;
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
// Get response from agent (uses code-defined config + injected history)
|
|
204
|
+
const response = await this.agent.ask(input, options);
|
|
205
|
+
|
|
206
|
+
// Update assistant message with response
|
|
207
|
+
return await model.update({
|
|
208
|
+
where: { id: assistantMessage.id },
|
|
209
|
+
data: {
|
|
210
|
+
content: response.content,
|
|
211
|
+
contentRaw: JSON.stringify(response.meta),
|
|
212
|
+
inputTokens: response.usage?.input_tokens || 0,
|
|
213
|
+
outputTokens: response.usage?.output_tokens || 0,
|
|
214
|
+
thinkingText: response.thinking?.text || null,
|
|
215
|
+
thinkingSignature: response.thinking?.signature || null,
|
|
216
|
+
thinkingTokens: response.thinking?.tokens || null,
|
|
217
|
+
modelId: response.model || null,
|
|
218
|
+
provider: response.provider || null
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
} catch (error) {
|
|
222
|
+
// Clean up placeholder on error
|
|
223
|
+
await model.delete({ where: { id: assistantMessage.id } });
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Stream a response and persist the conversation.
|
|
230
|
+
*/
|
|
231
|
+
async *askStream(
|
|
232
|
+
input: string,
|
|
233
|
+
options: AskOptions = {}
|
|
234
|
+
): AsyncGenerator<ChatChunk, MessageRecord, undefined> {
|
|
235
|
+
const model = this.getModel<MessageRecord>(this.tableNames.message);
|
|
236
|
+
|
|
237
|
+
// Persist user message
|
|
238
|
+
await model.create({
|
|
239
|
+
data: { chatId: this.chatId, role: "user", content: input }
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Create placeholder for assistant message
|
|
243
|
+
const assistantMessage = await model.create({
|
|
244
|
+
data: { chatId: this.chatId, role: "assistant", content: null }
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
this.currentMessageId = assistantMessage.id;
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
const stream = this.agent.stream(input, options);
|
|
251
|
+
|
|
252
|
+
let fullContent = "";
|
|
253
|
+
let lastChunk: ChatChunk | null = null;
|
|
254
|
+
|
|
255
|
+
for await (const chunk of stream) {
|
|
256
|
+
fullContent += chunk.content;
|
|
257
|
+
lastChunk = chunk;
|
|
258
|
+
yield chunk;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Final update with accumulated result
|
|
262
|
+
return await model.update({
|
|
263
|
+
where: { id: assistantMessage.id },
|
|
264
|
+
data: {
|
|
265
|
+
content: fullContent,
|
|
266
|
+
inputTokens: lastChunk?.usage?.input_tokens || 0,
|
|
267
|
+
outputTokens: lastChunk?.usage?.output_tokens || 0,
|
|
268
|
+
thinkingText: lastChunk?.thinking?.text || null,
|
|
269
|
+
thinkingSignature: lastChunk?.thinking?.signature || null,
|
|
270
|
+
thinkingTokens: lastChunk?.thinking?.tokens || null,
|
|
271
|
+
modelId: (lastChunk?.metadata?.model as string) || null,
|
|
272
|
+
provider: (lastChunk?.metadata?.provider as string) || null
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
} catch (error) {
|
|
276
|
+
await model.delete({ where: { id: assistantMessage.id } });
|
|
277
|
+
throw error;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Returns the current full message history for this session.
|
|
283
|
+
*/
|
|
284
|
+
async messages(): Promise<MessageRecord[]> {
|
|
285
|
+
const model = this.getModel<MessageRecord>(this.tableNames.message);
|
|
286
|
+
return await model.findMany({
|
|
287
|
+
where: { chatId: this.chatId },
|
|
288
|
+
orderBy: { createdAt: "asc" }
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Delete the entire session and its history.
|
|
294
|
+
*/
|
|
295
|
+
async delete(): Promise<void> {
|
|
296
|
+
const chatTable = this.getModel(this.tableNames.chat);
|
|
297
|
+
await chatTable.delete({ where: { id: this.chatId } });
|
|
298
|
+
// AgentSession record is deleted via Cascade from LlmChat
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Options for creating a new agent session.
|
|
304
|
+
*/
|
|
305
|
+
export interface CreateAgentSessionOptions {
|
|
306
|
+
metadata?: Record<string, unknown>;
|
|
307
|
+
tableNames?: TableNames;
|
|
308
|
+
debug?: boolean;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Creates a new agent session and its persistent chat record.
|
|
313
|
+
*/
|
|
314
|
+
export async function createAgentSession<T extends Agent>(
|
|
315
|
+
prisma: any,
|
|
316
|
+
llm: NodeLLMCore,
|
|
317
|
+
AgentClass: AgentClass<T>,
|
|
318
|
+
options: CreateAgentSessionOptions = {}
|
|
319
|
+
): Promise<AgentSession<T>> {
|
|
320
|
+
const tableNames = {
|
|
321
|
+
agentSession: options.tableNames?.agentSession || "llmAgentSession",
|
|
322
|
+
chat: options.tableNames?.chat || "llmChat",
|
|
323
|
+
message: options.tableNames?.message || "llmMessage"
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
if (options.debug) {
|
|
327
|
+
console.log(`[@node-llm/orm] createAgentSession: agentClass=${AgentClass.name}`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// 1. Create underlying LlmChat record
|
|
331
|
+
const chatTable = getTable(prisma, tableNames.chat);
|
|
332
|
+
const chatRecord = (await chatTable.create({
|
|
333
|
+
data: {
|
|
334
|
+
model: AgentClass.model || null,
|
|
335
|
+
provider: null,
|
|
336
|
+
instructions: AgentClass.instructions || null,
|
|
337
|
+
metadata: null // Runtime metadata goes in Chat, session context in AgentSession
|
|
338
|
+
}
|
|
339
|
+
})) as unknown as { id: string };
|
|
340
|
+
|
|
341
|
+
// 2. Create AgentSession record
|
|
342
|
+
const sessionTable = getTable(prisma, tableNames.agentSession);
|
|
343
|
+
const sessionRecord = (await sessionTable.create({
|
|
344
|
+
data: {
|
|
345
|
+
agentClass: AgentClass.name,
|
|
346
|
+
chatId: chatRecord.id,
|
|
347
|
+
metadata: options.metadata || null
|
|
348
|
+
}
|
|
349
|
+
})) as unknown as AgentSessionRecord;
|
|
350
|
+
|
|
351
|
+
return new AgentSession(
|
|
352
|
+
prisma,
|
|
353
|
+
llm,
|
|
354
|
+
AgentClass,
|
|
355
|
+
sessionRecord,
|
|
356
|
+
options.tableNames,
|
|
357
|
+
undefined,
|
|
358
|
+
options.debug
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Options for loading an existing agent session.
|
|
364
|
+
*/
|
|
365
|
+
export interface LoadAgentSessionOptions {
|
|
366
|
+
tableNames?: TableNames;
|
|
367
|
+
debug?: boolean;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Loads an existing agent session and re-instantiates the agent with history.
|
|
372
|
+
*/
|
|
373
|
+
export async function loadAgentSession<T extends Agent>(
|
|
374
|
+
prisma: any,
|
|
375
|
+
llm: NodeLLMCore,
|
|
376
|
+
AgentClass: AgentClass<T>,
|
|
377
|
+
sessionId: string,
|
|
378
|
+
options: LoadAgentSessionOptions = {}
|
|
379
|
+
): Promise<AgentSession<T> | null> {
|
|
380
|
+
const tableNames = {
|
|
381
|
+
agentSession: options.tableNames?.agentSession || "llmAgentSession",
|
|
382
|
+
chat: options.tableNames?.chat || "llmChat",
|
|
383
|
+
message: options.tableNames?.message || "llmMessage"
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
if (options.debug) {
|
|
387
|
+
console.log(`[@node-llm/orm] loadAgentSession: id=${sessionId}`);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// 1. Find session record
|
|
391
|
+
const sessionTable = getTable(prisma, tableNames.agentSession);
|
|
392
|
+
const sessionRecord = (await sessionTable.findUnique({
|
|
393
|
+
where: { id: sessionId }
|
|
394
|
+
})) as unknown as AgentSessionRecord | null;
|
|
395
|
+
|
|
396
|
+
if (!sessionRecord) {
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// 1.5. Validate Agent Class (Code Wins Sovereignty)
|
|
401
|
+
if (sessionRecord.agentClass !== AgentClass.name) {
|
|
402
|
+
throw new Error(
|
|
403
|
+
`Agent class mismatch: Session "${sessionId}" was created for "${sessionRecord.agentClass}", but is being loaded with "${AgentClass.name}".`
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// 2. Load message history
|
|
408
|
+
const messageTable = getTable(prisma, tableNames.message);
|
|
409
|
+
const messages = (await messageTable.findMany({
|
|
410
|
+
where: { chatId: sessionRecord.chatId },
|
|
411
|
+
orderBy: { createdAt: "asc" }
|
|
412
|
+
})) as unknown as MessageRecord[];
|
|
413
|
+
|
|
414
|
+
// 3. Convert DB messages to NodeLLM Message format
|
|
415
|
+
const history: Message[] = messages.map((m) => ({
|
|
416
|
+
role: m.role as "user" | "assistant" | "system",
|
|
417
|
+
content: m.content || ""
|
|
418
|
+
}));
|
|
419
|
+
|
|
420
|
+
// 4. Instantiate agent with injected history and LLM
|
|
421
|
+
// "Code Wins" - model, tools, instructions come from AgentClass
|
|
422
|
+
const agent = new AgentClass({
|
|
423
|
+
llm,
|
|
424
|
+
messages: history
|
|
425
|
+
}) as T;
|
|
426
|
+
|
|
427
|
+
return new AgentSession(
|
|
428
|
+
prisma,
|
|
429
|
+
llm,
|
|
430
|
+
AgentClass,
|
|
431
|
+
sessionRecord,
|
|
432
|
+
options.tableNames,
|
|
433
|
+
agent,
|
|
434
|
+
options.debug
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Dynamic helper to access Prisma models by name.
|
|
440
|
+
* Handles both case-sensitive and case-insensitive lookups for flexibility.
|
|
441
|
+
*/
|
|
442
|
+
function getTable(prisma: GenericPrismaClient, tableName: string): PrismaModel {
|
|
443
|
+
const p = prisma as unknown as Record<string, PrismaModel>;
|
|
444
|
+
|
|
445
|
+
// 1. Direct match
|
|
446
|
+
const table = p[tableName];
|
|
447
|
+
if (table) return table;
|
|
448
|
+
|
|
449
|
+
// 2. Case-insensitive match
|
|
450
|
+
const keys = Object.keys(prisma).filter((k) => !k.startsWith("$") && !k.startsWith("_"));
|
|
451
|
+
const match = keys.find((k) => k.toLowerCase() === tableName.toLowerCase());
|
|
452
|
+
|
|
453
|
+
if (match && p[match]) return p[match];
|
|
454
|
+
|
|
455
|
+
throw new Error(
|
|
456
|
+
`[@node-llm/orm] Prisma table "${tableName}" not found. Available tables: ${keys.join(", ")}`
|
|
457
|
+
);
|
|
458
|
+
}
|