@node-llm/orm 0.5.0 → 0.7.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 +72 -0
- package/dist/adapters/prisma/AgentSession.d.ts +42 -11
- package/dist/adapters/prisma/AgentSession.d.ts.map +1 -1
- package/dist/adapters/prisma/AgentSession.js +139 -13
- package/dist/adapters/prisma/Chat.d.ts +2 -2
- package/dist/adapters/prisma/Chat.d.ts.map +1 -1
- package/dist/adapters/prisma/Chat.js +6 -6
- package/package.json +2 -2
- package/src/adapters/prisma/AgentSession.ts +178 -32
- package/src/adapters/prisma/Chat.ts +6 -6
- package/test/AgentSession.test.ts +131 -3
- package/test/CodeWins.test.ts +1 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -6,7 +6,8 @@ import {
|
|
|
6
6
|
AgentConfig,
|
|
7
7
|
Message,
|
|
8
8
|
ChatChunk,
|
|
9
|
-
Usage
|
|
9
|
+
Usage,
|
|
10
|
+
ToolCall
|
|
10
11
|
} from "@node-llm/core";
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -72,13 +73,13 @@ interface PrismaModel<T = Record<string, unknown>> {
|
|
|
72
73
|
findUnique(args: { where: { id: string } }): Promise<T | null>;
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
type AgentClass<T extends Agent = Agent
|
|
76
|
-
overrides?: Partial<AgentConfig & ChatOptions>
|
|
76
|
+
type AgentClass<T extends Agent<any, any> = Agent<any, any>> = (new (
|
|
77
|
+
overrides?: Partial<AgentConfig<any> & ChatOptions>
|
|
77
78
|
) => T) & {
|
|
78
79
|
name: string;
|
|
79
80
|
model?: string;
|
|
80
|
-
instructions?:
|
|
81
|
-
tools?: unknown
|
|
81
|
+
instructions?: unknown;
|
|
82
|
+
tools?: unknown;
|
|
82
83
|
};
|
|
83
84
|
|
|
84
85
|
/**
|
|
@@ -87,6 +88,7 @@ type AgentClass<T extends Agent = Agent> = (new (
|
|
|
87
88
|
* Follows "Code Wins" sovereignty:
|
|
88
89
|
* - Model, Tools, Instructions come from the Agent class (code)
|
|
89
90
|
* - Message history comes from the database
|
|
91
|
+
* - Metadata from DB is injected as 'inputs' for dynamic resolution
|
|
90
92
|
*
|
|
91
93
|
* @example
|
|
92
94
|
* ```typescript
|
|
@@ -102,7 +104,10 @@ type AgentClass<T extends Agent = Agent> = (new (
|
|
|
102
104
|
* const result = await session.ask("Hello");
|
|
103
105
|
* ```
|
|
104
106
|
*/
|
|
105
|
-
export class AgentSession<
|
|
107
|
+
export class AgentSession<
|
|
108
|
+
I extends Record<string, any> = Record<string, any>,
|
|
109
|
+
T extends Agent<I, any> = Agent<I, any>
|
|
110
|
+
> {
|
|
106
111
|
private currentMessageId: string | null = null;
|
|
107
112
|
private tableNames: Required<TableNames>;
|
|
108
113
|
private debug: boolean;
|
|
@@ -114,7 +119,8 @@ export class AgentSession<T extends Agent = Agent> {
|
|
|
114
119
|
private record: AgentSessionRecord,
|
|
115
120
|
tableNames?: TableNames,
|
|
116
121
|
private agent: T = new AgentClass({
|
|
117
|
-
llm
|
|
122
|
+
llm,
|
|
123
|
+
inputs: record.metadata as I
|
|
118
124
|
}),
|
|
119
125
|
debug: boolean = false
|
|
120
126
|
) {
|
|
@@ -126,6 +132,8 @@ export class AgentSession<T extends Agent = Agent> {
|
|
|
126
132
|
toolCall: tableNames?.toolCall || "llmToolCall",
|
|
127
133
|
request: tableNames?.request || "llmRequest"
|
|
128
134
|
};
|
|
135
|
+
|
|
136
|
+
this.registerHooks();
|
|
129
137
|
}
|
|
130
138
|
|
|
131
139
|
private log(...args: any[]) {
|
|
@@ -150,8 +158,8 @@ export class AgentSession<T extends Agent = Agent> {
|
|
|
150
158
|
}
|
|
151
159
|
|
|
152
160
|
/** Session metadata */
|
|
153
|
-
get metadata():
|
|
154
|
-
return this.record.metadata;
|
|
161
|
+
get metadata(): I | null | undefined {
|
|
162
|
+
return this.record.metadata as I;
|
|
155
163
|
}
|
|
156
164
|
|
|
157
165
|
/** Agent class name */
|
|
@@ -181,15 +189,72 @@ export class AgentSession<T extends Agent = Agent> {
|
|
|
181
189
|
return getTable(this.prisma, name) as unknown as PrismaModel<R>;
|
|
182
190
|
}
|
|
183
191
|
|
|
192
|
+
/**
|
|
193
|
+
* Register persistence hooks on the agent.
|
|
194
|
+
*/
|
|
195
|
+
private registerHooks() {
|
|
196
|
+
this.agent.onToolCallStart(async (tc) => {
|
|
197
|
+
const toolCall = tc as ToolCall;
|
|
198
|
+
if (!this.currentMessageId) return;
|
|
199
|
+
const model = this.getModel(this.tableNames.toolCall);
|
|
200
|
+
await model.create({
|
|
201
|
+
data: {
|
|
202
|
+
messageId: this.currentMessageId,
|
|
203
|
+
toolCallId: toolCall.id,
|
|
204
|
+
name: toolCall.function.name,
|
|
205
|
+
arguments: toolCall.function.arguments
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
this.agent.onToolCallEnd(async (tc, result) => {
|
|
211
|
+
const toolCall = tc as ToolCall;
|
|
212
|
+
if (!this.currentMessageId) return;
|
|
213
|
+
const model = this.getModel(this.tableNames.toolCall);
|
|
214
|
+
try {
|
|
215
|
+
await model.update({
|
|
216
|
+
where: {
|
|
217
|
+
messageId_toolCallId: {
|
|
218
|
+
messageId: this.currentMessageId,
|
|
219
|
+
toolCallId: toolCall.id
|
|
220
|
+
}
|
|
221
|
+
} as any,
|
|
222
|
+
data: {
|
|
223
|
+
result: typeof result === "string" ? result : JSON.stringify(result)
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
} catch (e) {
|
|
227
|
+
this.log(`Failed to update tool call result: ${e}`);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
this.agent.afterResponse(async (response) => {
|
|
232
|
+
const model = this.getModel(this.tableNames.request);
|
|
233
|
+
await model.create({
|
|
234
|
+
data: {
|
|
235
|
+
chatId: this.chatId,
|
|
236
|
+
messageId: this.currentMessageId,
|
|
237
|
+
provider: response.provider || "unknown",
|
|
238
|
+
model: response.model || "unknown",
|
|
239
|
+
statusCode: 200,
|
|
240
|
+
duration: 0,
|
|
241
|
+
inputTokens: response.usage?.input_tokens || 0,
|
|
242
|
+
outputTokens: response.usage?.output_tokens || 0,
|
|
243
|
+
cost: response.usage?.cost || 0
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
184
249
|
/**
|
|
185
250
|
* Send a message and persist the conversation.
|
|
186
251
|
*/
|
|
187
|
-
async ask(
|
|
252
|
+
async ask(message: string, options: AskOptions & { inputs?: I } = {}): Promise<MessageRecord> {
|
|
188
253
|
const model = this.getModel<MessageRecord>(this.tableNames.message);
|
|
189
254
|
|
|
190
255
|
// Persist user message
|
|
191
256
|
await model.create({
|
|
192
|
-
data: { chatId: this.chatId, role: "user", content:
|
|
257
|
+
data: { chatId: this.chatId, role: "user", content: message }
|
|
193
258
|
});
|
|
194
259
|
|
|
195
260
|
// Create placeholder for assistant message
|
|
@@ -200,8 +265,11 @@ export class AgentSession<T extends Agent = Agent> {
|
|
|
200
265
|
this.currentMessageId = assistantMessage.id;
|
|
201
266
|
|
|
202
267
|
try {
|
|
268
|
+
// Merge turn-level inputs with session metadata
|
|
269
|
+
const inputs = { ...(this.record.metadata as I), ...options.inputs };
|
|
270
|
+
|
|
203
271
|
// Get response from agent (uses code-defined config + injected history)
|
|
204
|
-
const response = await this.agent.ask(
|
|
272
|
+
const response = await this.agent.ask(message, { ...options, inputs });
|
|
205
273
|
|
|
206
274
|
// Update assistant message with response
|
|
207
275
|
return await model.update({
|
|
@@ -229,14 +297,14 @@ export class AgentSession<T extends Agent = Agent> {
|
|
|
229
297
|
* Stream a response and persist the conversation.
|
|
230
298
|
*/
|
|
231
299
|
async *askStream(
|
|
232
|
-
|
|
233
|
-
options: AskOptions = {}
|
|
300
|
+
message: string,
|
|
301
|
+
options: AskOptions & { inputs?: I } = {}
|
|
234
302
|
): AsyncGenerator<ChatChunk, MessageRecord, undefined> {
|
|
235
303
|
const model = this.getModel<MessageRecord>(this.tableNames.message);
|
|
236
304
|
|
|
237
305
|
// Persist user message
|
|
238
306
|
await model.create({
|
|
239
|
-
data: { chatId: this.chatId, role: "user", content:
|
|
307
|
+
data: { chatId: this.chatId, role: "user", content: message }
|
|
240
308
|
});
|
|
241
309
|
|
|
242
310
|
// Create placeholder for assistant message
|
|
@@ -247,7 +315,9 @@ export class AgentSession<T extends Agent = Agent> {
|
|
|
247
315
|
this.currentMessageId = assistantMessage.id;
|
|
248
316
|
|
|
249
317
|
try {
|
|
250
|
-
|
|
318
|
+
// Merge turn-level inputs with session metadata
|
|
319
|
+
const inputs = { ...(this.record.metadata as I), ...options.inputs };
|
|
320
|
+
const stream = this.agent.stream(message, { ...options, inputs });
|
|
251
321
|
|
|
252
322
|
let fullContent = "";
|
|
253
323
|
let lastChunk: ChatChunk | null = null;
|
|
@@ -278,6 +348,44 @@ export class AgentSession<T extends Agent = Agent> {
|
|
|
278
348
|
}
|
|
279
349
|
}
|
|
280
350
|
|
|
351
|
+
/**
|
|
352
|
+
* Returns a usage summary for this chat session.
|
|
353
|
+
*/
|
|
354
|
+
async stats(): Promise<Usage> {
|
|
355
|
+
const requestModel = getTable(this.prisma, this.tableNames.request);
|
|
356
|
+
const aggregate = await (requestModel as any).aggregate({
|
|
357
|
+
where: { chatId: this.chatId },
|
|
358
|
+
_sum: {
|
|
359
|
+
inputTokens: true,
|
|
360
|
+
outputTokens: true,
|
|
361
|
+
cost: true
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
input_tokens: Number(aggregate._sum.inputTokens || 0),
|
|
367
|
+
output_tokens: Number(aggregate._sum.outputTokens || 0),
|
|
368
|
+
total_tokens: Number((aggregate._sum.inputTokens || 0) + (aggregate._sum.outputTokens || 0)),
|
|
369
|
+
cost: Number(aggregate._sum.cost || 0)
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Add a tool to the session (turn-level).
|
|
375
|
+
*/
|
|
376
|
+
withTool(tool: any): this {
|
|
377
|
+
this.agent.use(tool);
|
|
378
|
+
return this;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Add instructions to the session (turn-level).
|
|
383
|
+
*/
|
|
384
|
+
withInstructions(instructions: string, options?: { replace?: boolean }): this {
|
|
385
|
+
this.agent.withInstructions(instructions, options);
|
|
386
|
+
return this;
|
|
387
|
+
}
|
|
388
|
+
|
|
281
389
|
/**
|
|
282
390
|
* Returns the current full message history for this session.
|
|
283
391
|
*/
|
|
@@ -297,26 +405,50 @@ export class AgentSession<T extends Agent = Agent> {
|
|
|
297
405
|
await chatTable.delete({ where: { id: this.chatId } });
|
|
298
406
|
// AgentSession record is deleted via Cascade from LlmChat
|
|
299
407
|
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Update session metadata and re-resolve agent configuration.
|
|
411
|
+
*/
|
|
412
|
+
async updateMetadata(metadata: Partial<I>): Promise<void> {
|
|
413
|
+
const sessionTable = this.getModel<AgentSessionRecord>(this.tableNames.agentSession);
|
|
414
|
+
const newMetadata = { ...(this.record.metadata as I), ...metadata };
|
|
415
|
+
|
|
416
|
+
await sessionTable.update({
|
|
417
|
+
where: { id: this.id },
|
|
418
|
+
data: { metadata: newMetadata as any }
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
this.record.metadata = newMetadata as any;
|
|
422
|
+
|
|
423
|
+
// Apply changes to the underlying agent immediately
|
|
424
|
+
// resolveLazyConfig is private, so we need a cast or make it protected.
|
|
425
|
+
// Given we are in the same package, we can cast.
|
|
426
|
+
(this.agent as any).resolveLazyConfig(newMetadata);
|
|
427
|
+
}
|
|
300
428
|
}
|
|
301
429
|
|
|
302
430
|
/**
|
|
303
431
|
* Options for creating a new agent session.
|
|
304
432
|
*/
|
|
305
|
-
export interface CreateAgentSessionOptions {
|
|
306
|
-
metadata?:
|
|
433
|
+
export interface CreateAgentSessionOptions<I = any> {
|
|
434
|
+
metadata?: I;
|
|
307
435
|
tableNames?: TableNames;
|
|
308
436
|
debug?: boolean;
|
|
437
|
+
model?: string;
|
|
438
|
+
provider?: string;
|
|
439
|
+
instructions?: string;
|
|
440
|
+
maxToolCalls?: number;
|
|
309
441
|
}
|
|
310
442
|
|
|
311
443
|
/**
|
|
312
444
|
* Creates a new agent session and its persistent chat record.
|
|
313
445
|
*/
|
|
314
|
-
export async function createAgentSession<T extends Agent
|
|
446
|
+
export async function createAgentSession<I extends Record<string, any>, T extends Agent<I, any>>(
|
|
315
447
|
prisma: any,
|
|
316
448
|
llm: NodeLLMCore,
|
|
317
449
|
AgentClass: AgentClass<T>,
|
|
318
|
-
options: CreateAgentSessionOptions = {}
|
|
319
|
-
): Promise<AgentSession<T>> {
|
|
450
|
+
options: CreateAgentSessionOptions<I> = {}
|
|
451
|
+
): Promise<AgentSession<I, T>> {
|
|
320
452
|
const tableNames = {
|
|
321
453
|
agentSession: options.tableNames?.agentSession || "llmAgentSession",
|
|
322
454
|
chat: options.tableNames?.chat || "llmChat",
|
|
@@ -331,9 +463,11 @@ export async function createAgentSession<T extends Agent>(
|
|
|
331
463
|
const chatTable = getTable(prisma, tableNames.chat);
|
|
332
464
|
const chatRecord = (await chatTable.create({
|
|
333
465
|
data: {
|
|
334
|
-
model: AgentClass.model || null,
|
|
335
|
-
provider: null,
|
|
336
|
-
instructions:
|
|
466
|
+
model: options.model || AgentClass.model || null,
|
|
467
|
+
provider: options.provider || null,
|
|
468
|
+
instructions:
|
|
469
|
+
options.instructions ||
|
|
470
|
+
(typeof AgentClass.instructions === "string" ? AgentClass.instructions : null),
|
|
337
471
|
metadata: null // Runtime metadata goes in Chat, session context in AgentSession
|
|
338
472
|
}
|
|
339
473
|
})) as unknown as { id: string };
|
|
@@ -344,17 +478,27 @@ export async function createAgentSession<T extends Agent>(
|
|
|
344
478
|
data: {
|
|
345
479
|
agentClass: AgentClass.name,
|
|
346
480
|
chatId: chatRecord.id,
|
|
347
|
-
metadata: options.metadata || null
|
|
481
|
+
metadata: (options.metadata as any) || null
|
|
348
482
|
}
|
|
349
483
|
})) as unknown as AgentSessionRecord;
|
|
350
484
|
|
|
351
|
-
|
|
485
|
+
// 3. Instantiate Agent with overrides
|
|
486
|
+
const agent = new AgentClass({
|
|
487
|
+
llm,
|
|
488
|
+
inputs: sessionRecord.metadata as I,
|
|
489
|
+
model: options.model,
|
|
490
|
+
provider: options.provider,
|
|
491
|
+
instructions: options.instructions,
|
|
492
|
+
maxToolCalls: options.maxToolCalls
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
return new AgentSession<I, T>(
|
|
352
496
|
prisma,
|
|
353
497
|
llm,
|
|
354
498
|
AgentClass,
|
|
355
499
|
sessionRecord,
|
|
356
500
|
options.tableNames,
|
|
357
|
-
|
|
501
|
+
agent,
|
|
358
502
|
options.debug
|
|
359
503
|
);
|
|
360
504
|
}
|
|
@@ -370,13 +514,13 @@ export interface LoadAgentSessionOptions {
|
|
|
370
514
|
/**
|
|
371
515
|
* Loads an existing agent session and re-instantiates the agent with history.
|
|
372
516
|
*/
|
|
373
|
-
export async function loadAgentSession<T extends Agent
|
|
517
|
+
export async function loadAgentSession<I extends Record<string, any>, T extends Agent<I, any>>(
|
|
374
518
|
prisma: any,
|
|
375
519
|
llm: NodeLLMCore,
|
|
376
520
|
AgentClass: AgentClass<T>,
|
|
377
521
|
sessionId: string,
|
|
378
522
|
options: LoadAgentSessionOptions = {}
|
|
379
|
-
): Promise<AgentSession<T> | null> {
|
|
523
|
+
): Promise<AgentSession<I, T> | null> {
|
|
380
524
|
const tableNames = {
|
|
381
525
|
agentSession: options.tableNames?.agentSession || "llmAgentSession",
|
|
382
526
|
chat: options.tableNames?.chat || "llmChat",
|
|
@@ -417,14 +561,16 @@ export async function loadAgentSession<T extends Agent>(
|
|
|
417
561
|
content: m.content || ""
|
|
418
562
|
}));
|
|
419
563
|
|
|
420
|
-
// 4. Instantiate agent with injected history
|
|
564
|
+
// 4. Instantiate agent with injected history, LLM, AND metadata (as inputs)
|
|
421
565
|
// "Code Wins" - model, tools, instructions come from AgentClass
|
|
566
|
+
// Metadata from DB handles the lazy resolution of behavior
|
|
422
567
|
const agent = new AgentClass({
|
|
423
568
|
llm,
|
|
424
|
-
messages: history
|
|
569
|
+
messages: history,
|
|
570
|
+
inputs: sessionRecord.metadata as I
|
|
425
571
|
}) as T;
|
|
426
572
|
|
|
427
|
-
return new AgentSession(
|
|
573
|
+
return new AgentSession<I, T>(
|
|
428
574
|
prisma,
|
|
429
575
|
llm,
|
|
430
576
|
AgentClass,
|
|
@@ -180,10 +180,10 @@ export class Chat extends BaseChat {
|
|
|
180
180
|
/**
|
|
181
181
|
* Send a message and persist the conversation.
|
|
182
182
|
*/
|
|
183
|
-
async ask(
|
|
183
|
+
async ask(message: string, options: AskOptions = {}): Promise<MessageRecord> {
|
|
184
184
|
const messageModel = this.tables.message;
|
|
185
185
|
const userMessage = await (this.prisma as any)[messageModel].create({
|
|
186
|
-
data: { chatId: this.id, role: "user", content:
|
|
186
|
+
data: { chatId: this.id, role: "user", content: message }
|
|
187
187
|
});
|
|
188
188
|
|
|
189
189
|
const assistantMessage = await (this.prisma as any)[messageModel].create({
|
|
@@ -202,7 +202,7 @@ export class Chat extends BaseChat {
|
|
|
202
202
|
}));
|
|
203
203
|
|
|
204
204
|
const coreChat = await this.prepareCoreChat(history, assistantMessage!.id);
|
|
205
|
-
const response = await coreChat.ask(
|
|
205
|
+
const response = await coreChat.ask(message, options);
|
|
206
206
|
|
|
207
207
|
return await (this.prisma as any)[messageModel].update({
|
|
208
208
|
where: { id: assistantMessage!.id },
|
|
@@ -231,12 +231,12 @@ export class Chat extends BaseChat {
|
|
|
231
231
|
* Yields ChatChunk objects for full visibility of thinking, content, and tools.
|
|
232
232
|
*/
|
|
233
233
|
async *askStream(
|
|
234
|
-
|
|
234
|
+
message: string,
|
|
235
235
|
options: AskOptions = {}
|
|
236
236
|
): AsyncGenerator<ChatChunk, MessageRecord, undefined> {
|
|
237
237
|
const messageModel = this.tables.message;
|
|
238
238
|
const userMessage = await (this.prisma as any)[messageModel].create({
|
|
239
|
-
data: { chatId: this.id, role: "user", content:
|
|
239
|
+
data: { chatId: this.id, role: "user", content: message }
|
|
240
240
|
});
|
|
241
241
|
|
|
242
242
|
const assistantMessage = await (this.prisma as any)[messageModel].create({
|
|
@@ -255,7 +255,7 @@ export class Chat extends BaseChat {
|
|
|
255
255
|
}));
|
|
256
256
|
|
|
257
257
|
const coreChat = await this.prepareCoreChat(history, assistantMessage!.id);
|
|
258
|
-
const stream = coreChat.stream(
|
|
258
|
+
const stream = coreChat.stream(message, options);
|
|
259
259
|
|
|
260
260
|
let fullContent = "";
|
|
261
261
|
let metadata: any = {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
2
|
import { Agent, Tool, NodeLLM } from "@node-llm/core";
|
|
3
|
-
import { createAgentSession, loadAgentSession } from "../src/adapters/prisma/AgentSession";
|
|
3
|
+
import { createAgentSession, loadAgentSession } from "../src/adapters/prisma/AgentSession.js";
|
|
4
4
|
|
|
5
5
|
// --- Mocks ---
|
|
6
6
|
|
|
@@ -12,7 +12,8 @@ const mockPrisma = {
|
|
|
12
12
|
},
|
|
13
13
|
llmAgentSession: {
|
|
14
14
|
create: vi.fn(),
|
|
15
|
-
findUnique: vi.fn()
|
|
15
|
+
findUnique: vi.fn(),
|
|
16
|
+
update: vi.fn()
|
|
16
17
|
},
|
|
17
18
|
llmMessage: {
|
|
18
19
|
create: vi.fn(),
|
|
@@ -42,7 +43,8 @@ const createMockChat = () => {
|
|
|
42
43
|
onToolCallStart: vi.fn().mockReturnThis(),
|
|
43
44
|
onToolCallEnd: vi.fn().mockReturnThis(),
|
|
44
45
|
onToolCallError: vi.fn().mockReturnThis(),
|
|
45
|
-
onEndMessage: vi.fn().mockReturnThis()
|
|
46
|
+
onEndMessage: vi.fn().mockReturnThis(),
|
|
47
|
+
afterResponse: vi.fn().mockReturnThis()
|
|
46
48
|
};
|
|
47
49
|
return mockChat;
|
|
48
50
|
};
|
|
@@ -201,4 +203,130 @@ describe("AgentSession", () => {
|
|
|
201
203
|
);
|
|
202
204
|
});
|
|
203
205
|
});
|
|
206
|
+
|
|
207
|
+
describe("Lazy Evaluation & Metadata", () => {
|
|
208
|
+
interface TestInputs {
|
|
209
|
+
userName: string;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
class LazyTestAgent extends Agent<TestInputs> {
|
|
213
|
+
static model = "gpt-4-lazy";
|
|
214
|
+
static instructions = (i: TestInputs) => `Hello ${i.userName}`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
it("injects metadata as inputs for lazy resolution during load", async () => {
|
|
218
|
+
mockPrisma.llmAgentSession.findUnique.mockResolvedValue({
|
|
219
|
+
id: "session-123",
|
|
220
|
+
chatId: "chat-123",
|
|
221
|
+
agentClass: "LazyTestAgent",
|
|
222
|
+
metadata: { userName: "Alice" }
|
|
223
|
+
});
|
|
224
|
+
mockPrisma.llmMessage.findMany.mockResolvedValue([]);
|
|
225
|
+
|
|
226
|
+
const session = await loadAgentSession(
|
|
227
|
+
mockPrisma as any,
|
|
228
|
+
mockLlm,
|
|
229
|
+
LazyTestAgent as any,
|
|
230
|
+
"session-123"
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
// Extract the underlying agent's chat instance
|
|
234
|
+
const mockChat = (session as any).agent.chat;
|
|
235
|
+
expect(mockChat.withInstructions).toHaveBeenCalledWith("Hello Alice", { replace: true });
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("merges turn-level inputs with session metadata during ask()", async () => {
|
|
239
|
+
mockPrisma.llmAgentSession.findUnique.mockResolvedValue({
|
|
240
|
+
id: "session-123",
|
|
241
|
+
chatId: "chat-123",
|
|
242
|
+
agentClass: "LazyTestAgent",
|
|
243
|
+
metadata: { userName: "Bob" }
|
|
244
|
+
});
|
|
245
|
+
mockPrisma.llmMessage.findMany.mockResolvedValue([]);
|
|
246
|
+
mockPrisma.llmMessage.create.mockResolvedValue({ id: "msg" });
|
|
247
|
+
mockPrisma.llmMessage.update.mockResolvedValue({ id: "msg" });
|
|
248
|
+
|
|
249
|
+
const session = (await loadAgentSession(
|
|
250
|
+
mockPrisma as any,
|
|
251
|
+
mockLlm,
|
|
252
|
+
LazyTestAgent as any,
|
|
253
|
+
"session-123"
|
|
254
|
+
))!;
|
|
255
|
+
|
|
256
|
+
// Mock the instructions resolver again to proof turn-level override
|
|
257
|
+
LazyTestAgent.instructions = (i: any) => `Hi ${i.userName}, turn: ${i.turn}`;
|
|
258
|
+
|
|
259
|
+
await session.ask("Hello", { inputs: { turn: "1" } } as any);
|
|
260
|
+
|
|
261
|
+
const mockChat = (session as any).agent.chat;
|
|
262
|
+
expect(mockChat.ask).toHaveBeenCalledWith(
|
|
263
|
+
"Hello",
|
|
264
|
+
expect.objectContaining({
|
|
265
|
+
inputs: expect.objectContaining({
|
|
266
|
+
userName: "Bob",
|
|
267
|
+
turn: "1"
|
|
268
|
+
})
|
|
269
|
+
})
|
|
270
|
+
);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
describe("Delegation & Metadata", () => {
|
|
275
|
+
it("delegates withTool to the underlying agent", async () => {
|
|
276
|
+
mockPrisma.llmAgentSession.findUnique.mockResolvedValue({
|
|
277
|
+
agentClass: "TestAgent",
|
|
278
|
+
metadata: {}
|
|
279
|
+
});
|
|
280
|
+
mockPrisma.llmMessage.findMany.mockResolvedValue([]);
|
|
281
|
+
|
|
282
|
+
const session = (await loadAgentSession(mockPrisma as any, mockLlm, TestAgent, "123"))!;
|
|
283
|
+
session.withTool({ name: "extra-tool" });
|
|
284
|
+
|
|
285
|
+
expect((session as any).agent.chat.withTools).toHaveBeenCalledWith(
|
|
286
|
+
[{ name: "extra-tool" }],
|
|
287
|
+
undefined
|
|
288
|
+
);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("updates metadata and re-resolves lazy config", async () => {
|
|
292
|
+
class LazyAgent extends Agent<{ color: string }> {
|
|
293
|
+
static model = "mock-model";
|
|
294
|
+
static instructions = (i: any) => `Color is ${i.color}`;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
mockPrisma.llmAgentSession.findUnique.mockResolvedValue({
|
|
298
|
+
id: "123",
|
|
299
|
+
agentClass: "LazyAgent",
|
|
300
|
+
metadata: { color: "red" }
|
|
301
|
+
});
|
|
302
|
+
mockPrisma.llmMessage.findMany.mockResolvedValue([]);
|
|
303
|
+
mockPrisma.llmAgentSession.update = vi.fn().mockResolvedValue({});
|
|
304
|
+
|
|
305
|
+
const session = (await loadAgentSession(
|
|
306
|
+
mockPrisma as any,
|
|
307
|
+
mockLlm,
|
|
308
|
+
LazyAgent as any,
|
|
309
|
+
"123"
|
|
310
|
+
))!;
|
|
311
|
+
|
|
312
|
+
// Initial resolution
|
|
313
|
+
expect((session as any).agent.chat.withInstructions).toHaveBeenCalledWith("Color is red", {
|
|
314
|
+
replace: true
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
await session.updateMetadata({ color: "blue" });
|
|
318
|
+
|
|
319
|
+
// Verify DB update
|
|
320
|
+
expect(mockPrisma.llmAgentSession.update).toHaveBeenCalledWith(
|
|
321
|
+
expect.objectContaining({
|
|
322
|
+
data: { metadata: { color: "blue" } }
|
|
323
|
+
})
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
// Verify re-resolution
|
|
327
|
+
expect((session as any).agent.chat.withInstructions).toHaveBeenCalledWith("Color is blue", {
|
|
328
|
+
replace: true
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
});
|
|
204
332
|
});
|
package/test/CodeWins.test.ts
CHANGED