@rudderjs/ai 1.11.1 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.d.ts +119 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +74 -0
- package/dist/agent.js.map +1 -1
- package/dist/chat-mentions.d.ts +59 -0
- package/dist/chat-mentions.d.ts.map +1 -0
- package/dist/chat-mentions.js +81 -0
- package/dist/chat-mentions.js.map +1 -0
- package/dist/continuation-validation.d.ts +85 -0
- package/dist/continuation-validation.d.ts.map +1 -0
- package/dist/continuation-validation.js +166 -0
- package/dist/continuation-validation.js.map +1 -0
- package/dist/conversation-orm/index.d.ts +109 -0
- package/dist/conversation-orm/index.d.ts.map +1 -0
- package/dist/conversation-orm/index.js +211 -0
- package/dist/conversation-orm/index.js.map +1 -0
- package/dist/conversation-persistence.d.ts.map +1 -1
- package/dist/conversation-persistence.js +27 -3
- package/dist/conversation-persistence.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/providers/openai.d.ts +35 -0
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +84 -1
- package/dist/providers/openai.js.map +1 -1
- package/dist/types.d.ts +31 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +15 -7
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { AiMessage, ContinuationValidator, ValidateContinuationOptions } from './types.js';
|
|
2
|
+
export type { ContinuationValidator, ValidateContinuationOptions } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Continuation validation — defends the auto-persist / continuation path
|
|
5
|
+
* against a client that resubmits a forged conversation.
|
|
6
|
+
*
|
|
7
|
+
* `runWithPersistence` (and the explicit `forUser`/`continue` form) trusts
|
|
8
|
+
* the caller's incoming history. A continuation request after a client-tool
|
|
9
|
+
* or approval round-trip carries the prior messages back from the browser,
|
|
10
|
+
* which means a malicious caller can:
|
|
11
|
+
*
|
|
12
|
+
* 1. **Rewrite history / continue someone else's thread (IDOR)** — send
|
|
13
|
+
* messages that don't match what the server persisted for this thread.
|
|
14
|
+
* 2. **Forge a tool result** — append a `tool` message answering a tool
|
|
15
|
+
* call that the server never issued (smuggling attacker-chosen data in
|
|
16
|
+
* as if a tool produced it).
|
|
17
|
+
* 3. **Forge an approval** — claim `approvedToolCallIds` for a tool call
|
|
18
|
+
* that isn't actually pending approval.
|
|
19
|
+
*
|
|
20
|
+
* {@link validateContinuation} runs all three checks against the trusted
|
|
21
|
+
* `persisted` history and returns a verdict. {@link assertValidContinuation}
|
|
22
|
+
* throws {@link ContinuationValidationError} on failure, and
|
|
23
|
+
* {@link defaultContinuationValidator} adapts it to the
|
|
24
|
+
* {@link ContinuationValidator} hook shape consumed by `AgentPromptOptions`.
|
|
25
|
+
*/
|
|
26
|
+
export type ContinuationRejectionCode = 'not-a-prefix' | 'forged-tool-result' | 'forged-approval';
|
|
27
|
+
export interface ContinuationValidationResult {
|
|
28
|
+
/** `true` when the incoming continuation is a legitimate extension of `persisted`. */
|
|
29
|
+
ok: boolean;
|
|
30
|
+
/** Machine-readable reason, present only when `ok` is `false`. */
|
|
31
|
+
code?: ContinuationRejectionCode;
|
|
32
|
+
/** Human-readable explanation, present only when `ok` is `false`. */
|
|
33
|
+
reason?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Index of the offending message — into `incoming` for `not-a-prefix` and
|
|
36
|
+
* `forged-tool-result`. Absent for `forged-approval` (the offending id is
|
|
37
|
+
* named in `reason` instead).
|
|
38
|
+
*/
|
|
39
|
+
index?: number;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Validate that an `incoming` continuation is a legitimate extension of the
|
|
43
|
+
* server-persisted `persisted` history. Pure and synchronous — safe to call
|
|
44
|
+
* from any runtime. Returns a verdict; never throws.
|
|
45
|
+
*
|
|
46
|
+
* Checks, in order:
|
|
47
|
+
*
|
|
48
|
+
* - **Prefix equality.** Every message the two share by position must match
|
|
49
|
+
* (role, content, `toolCallId`, and any assistant `toolCalls`). Comparison
|
|
50
|
+
* is order-insensitive for nested objects, so a tool-call `arguments` map
|
|
51
|
+
* reordered across a serialization boundary still matches. A genuine
|
|
52
|
+
* mismatch means the caller rewrote history or is replaying a different
|
|
53
|
+
* thread (IDOR) → `not-a-prefix`.
|
|
54
|
+
* - **Tool-result forgery.** Every `tool` message in `incoming` must answer
|
|
55
|
+
* a tool call actually requested by some assistant message (in either
|
|
56
|
+
* `persisted` or `incoming`). A `tool` message with no matching request is
|
|
57
|
+
* smuggled data → `forged-tool-result`.
|
|
58
|
+
* - **Approval forgery.** Every id in `opts.approvedToolCallIds` /
|
|
59
|
+
* `opts.rejectedToolCallIds` must reference a real requested tool call →
|
|
60
|
+
* `forged-approval`.
|
|
61
|
+
*/
|
|
62
|
+
export declare function validateContinuation(persisted: readonly AiMessage[], incoming: readonly AiMessage[], opts?: ValidateContinuationOptions): ContinuationValidationResult;
|
|
63
|
+
/** Thrown by {@link assertValidContinuation} when validation fails. */
|
|
64
|
+
export declare class ContinuationValidationError extends Error {
|
|
65
|
+
readonly code: ContinuationRejectionCode;
|
|
66
|
+
readonly index: number | undefined;
|
|
67
|
+
constructor(result: ContinuationValidationResult);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* {@link validateContinuation} that throws {@link ContinuationValidationError}
|
|
71
|
+
* instead of returning a verdict. Use directly, or via
|
|
72
|
+
* {@link defaultContinuationValidator} as a `validate` hook.
|
|
73
|
+
*/
|
|
74
|
+
export declare function assertValidContinuation(persisted: readonly AiMessage[], incoming: readonly AiMessage[], opts?: ValidateContinuationOptions): void;
|
|
75
|
+
/**
|
|
76
|
+
* A ready-made {@link ContinuationValidator} backed by
|
|
77
|
+
* {@link assertValidContinuation}. Drop into `AgentPromptOptions.validate`
|
|
78
|
+
* for the default prefix + tool-result-forgery + approval-forgery gate:
|
|
79
|
+
*
|
|
80
|
+
* ```ts
|
|
81
|
+
* agent.continue(id).prompt(input, { validate: defaultContinuationValidator() })
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export declare function defaultContinuationValidator(): ContinuationValidator;
|
|
85
|
+
//# sourceMappingURL=continuation-validation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"continuation-validation.d.ts","sourceRoot":"","sources":["../src/continuation-validation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,qBAAqB,EAAY,2BAA2B,EAAE,MAAM,YAAY,CAAA;AAEzG,YAAY,EAAE,qBAAqB,EAAE,2BAA2B,EAAE,MAAM,YAAY,CAAA;AAEpF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,MAAM,MAAM,yBAAyB,GACjC,cAAc,GACd,oBAAoB,GACpB,iBAAiB,CAAA;AAErB,MAAM,WAAW,4BAA4B;IAC3C,sFAAsF;IACtF,EAAE,EAAE,OAAO,CAAA;IACX,kEAAkE;IAClE,IAAI,CAAC,EAAE,yBAAyB,CAAA;IAChC,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAmED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,SAAS,SAAS,EAAE,EAC/B,QAAQ,EAAE,SAAS,SAAS,EAAE,EAC9B,IAAI,GAAE,2BAAgC,GACrC,4BAA4B,CA2C9B;AAED,uEAAuE;AACvE,qBAAa,2BAA4B,SAAQ,KAAK;IACpD,QAAQ,CAAC,IAAI,EAAE,yBAAyB,CAAA;IACxC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAA;gBACtB,MAAM,EAAE,4BAA4B;CAMjD;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,SAAS,SAAS,EAAE,EAC/B,QAAQ,EAAE,SAAS,SAAS,EAAE,EAC9B,IAAI,GAAE,2BAAgC,GACrC,IAAI,CAGN;AAED;;;;;;;;GAQG;AACH,wBAAgB,4BAA4B,IAAI,qBAAqB,CAEpE"}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recursively sort object keys (and drop `undefined`) so two semantically
|
|
3
|
+
* equal values compare equal regardless of key insertion order. Without this,
|
|
4
|
+
* a tool-call `arguments` object reordered across a serialization boundary
|
|
5
|
+
* (e.g. loaded back from a Postgres `jsonb` column, which does not preserve
|
|
6
|
+
* key order, or rebuilt by the client) would be read as a forgery.
|
|
7
|
+
*/
|
|
8
|
+
function canonicalize(value) {
|
|
9
|
+
if (value === null || typeof value !== 'object')
|
|
10
|
+
return value;
|
|
11
|
+
if (Array.isArray(value))
|
|
12
|
+
return value.map(canonicalize);
|
|
13
|
+
const out = {};
|
|
14
|
+
for (const key of Object.keys(value).sort()) {
|
|
15
|
+
const v = value[key];
|
|
16
|
+
if (v === undefined)
|
|
17
|
+
continue;
|
|
18
|
+
out[key] = canonicalize(v);
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
/** Order-insensitive JSON of a value. */
|
|
23
|
+
function canonicalJson(value) {
|
|
24
|
+
return JSON.stringify(canonicalize(value));
|
|
25
|
+
}
|
|
26
|
+
/** Order-insensitive string form of message content. */
|
|
27
|
+
function contentString(content) {
|
|
28
|
+
return typeof content === 'string' ? content : canonicalJson(content);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Compare two messages field by field, order-insensitively. Returns a
|
|
32
|
+
* human-readable reason naming the first diverging field, or `null` when the
|
|
33
|
+
* messages are equivalent. Used by the prefix check so a rejection points at
|
|
34
|
+
* exactly what diverged.
|
|
35
|
+
*/
|
|
36
|
+
function messageDiffReason(a, b) {
|
|
37
|
+
if (a.role !== b.role)
|
|
38
|
+
return `role: persisted "${a.role}" vs incoming "${b.role}"`;
|
|
39
|
+
if (contentString(a.content) !== contentString(b.content))
|
|
40
|
+
return 'content differs';
|
|
41
|
+
if ((a.toolCallId ?? null) !== (b.toolCallId ?? null)) {
|
|
42
|
+
return `toolCallId: persisted "${a.toolCallId ?? 'null'}" vs incoming "${b.toolCallId ?? 'null'}"`;
|
|
43
|
+
}
|
|
44
|
+
const aCalls = a.toolCalls ?? [];
|
|
45
|
+
const bCalls = b.toolCalls ?? [];
|
|
46
|
+
if (aCalls.length !== bCalls.length)
|
|
47
|
+
return `toolCalls length: persisted ${aCalls.length} vs incoming ${bCalls.length}`;
|
|
48
|
+
for (let i = 0; i < aCalls.length; i++) {
|
|
49
|
+
const ac = aCalls[i];
|
|
50
|
+
const bc = bCalls[i];
|
|
51
|
+
if (ac.id !== bc.id)
|
|
52
|
+
return `toolCalls[${i}].id: persisted "${ac.id}" vs incoming "${bc.id}"`;
|
|
53
|
+
if (ac.name !== bc.name)
|
|
54
|
+
return `toolCalls[${i}].name: persisted "${ac.name}" vs incoming "${bc.name}"`;
|
|
55
|
+
if (canonicalJson(ac.arguments) !== canonicalJson(bc.arguments))
|
|
56
|
+
return `toolCalls[${i}].arguments differ (${ac.name})`;
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
/** Collect every tool-call id the model requested across the given messages. */
|
|
61
|
+
function requestedToolCallIds(messages) {
|
|
62
|
+
const ids = new Set();
|
|
63
|
+
for (const m of messages) {
|
|
64
|
+
if (m.role === 'assistant' && m.toolCalls) {
|
|
65
|
+
for (const c of m.toolCalls)
|
|
66
|
+
ids.add(c.id);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return ids;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Validate that an `incoming` continuation is a legitimate extension of the
|
|
73
|
+
* server-persisted `persisted` history. Pure and synchronous — safe to call
|
|
74
|
+
* from any runtime. Returns a verdict; never throws.
|
|
75
|
+
*
|
|
76
|
+
* Checks, in order:
|
|
77
|
+
*
|
|
78
|
+
* - **Prefix equality.** Every message the two share by position must match
|
|
79
|
+
* (role, content, `toolCallId`, and any assistant `toolCalls`). Comparison
|
|
80
|
+
* is order-insensitive for nested objects, so a tool-call `arguments` map
|
|
81
|
+
* reordered across a serialization boundary still matches. A genuine
|
|
82
|
+
* mismatch means the caller rewrote history or is replaying a different
|
|
83
|
+
* thread (IDOR) → `not-a-prefix`.
|
|
84
|
+
* - **Tool-result forgery.** Every `tool` message in `incoming` must answer
|
|
85
|
+
* a tool call actually requested by some assistant message (in either
|
|
86
|
+
* `persisted` or `incoming`). A `tool` message with no matching request is
|
|
87
|
+
* smuggled data → `forged-tool-result`.
|
|
88
|
+
* - **Approval forgery.** Every id in `opts.approvedToolCallIds` /
|
|
89
|
+
* `opts.rejectedToolCallIds` must reference a real requested tool call →
|
|
90
|
+
* `forged-approval`.
|
|
91
|
+
*/
|
|
92
|
+
export function validateContinuation(persisted, incoming, opts = {}) {
|
|
93
|
+
// 1. Prefix equality over the shared region. Comparison is order-insensitive
|
|
94
|
+
// for nested objects (tool-call arguments, structured content) so a
|
|
95
|
+
// legitimately reordered argument map is not mistaken for a forgery.
|
|
96
|
+
const overlap = Math.min(persisted.length, incoming.length);
|
|
97
|
+
for (let i = 0; i < overlap; i++) {
|
|
98
|
+
const diff = messageDiffReason(persisted[i], incoming[i]);
|
|
99
|
+
if (diff) {
|
|
100
|
+
return {
|
|
101
|
+
ok: false,
|
|
102
|
+
code: 'not-a-prefix',
|
|
103
|
+
index: i,
|
|
104
|
+
reason: `incoming message at index ${i} diverges from the persisted history (${diff})`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// 2. Tool-result forgery — a tool message must answer a requested call.
|
|
109
|
+
const requested = requestedToolCallIds([...persisted, ...incoming]);
|
|
110
|
+
for (let i = 0; i < incoming.length; i++) {
|
|
111
|
+
const m = incoming[i];
|
|
112
|
+
if (m.role === 'tool' && (!m.toolCallId || !requested.has(m.toolCallId))) {
|
|
113
|
+
return {
|
|
114
|
+
ok: false,
|
|
115
|
+
code: 'forged-tool-result',
|
|
116
|
+
index: i,
|
|
117
|
+
reason: `tool message at index ${i} references tool call "${m.toolCallId ?? '<missing>'}" that was never requested`,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// 3. Approval forgery — approved/rejected ids must be real requested calls.
|
|
122
|
+
for (const id of [...(opts.approvedToolCallIds ?? []), ...(opts.rejectedToolCallIds ?? [])]) {
|
|
123
|
+
if (!requested.has(id)) {
|
|
124
|
+
return {
|
|
125
|
+
ok: false,
|
|
126
|
+
code: 'forged-approval',
|
|
127
|
+
reason: `tool call "${id}" was approved or rejected but was never requested`,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return { ok: true };
|
|
132
|
+
}
|
|
133
|
+
/** Thrown by {@link assertValidContinuation} when validation fails. */
|
|
134
|
+
export class ContinuationValidationError extends Error {
|
|
135
|
+
code;
|
|
136
|
+
index;
|
|
137
|
+
constructor(result) {
|
|
138
|
+
super(`[RudderJS AI] Rejected continuation: ${result.reason ?? result.code ?? 'invalid'}`);
|
|
139
|
+
this.name = 'ContinuationValidationError';
|
|
140
|
+
this.code = result.code ?? 'not-a-prefix';
|
|
141
|
+
this.index = result.index;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* {@link validateContinuation} that throws {@link ContinuationValidationError}
|
|
146
|
+
* instead of returning a verdict. Use directly, or via
|
|
147
|
+
* {@link defaultContinuationValidator} as a `validate` hook.
|
|
148
|
+
*/
|
|
149
|
+
export function assertValidContinuation(persisted, incoming, opts = {}) {
|
|
150
|
+
const result = validateContinuation(persisted, incoming, opts);
|
|
151
|
+
if (!result.ok)
|
|
152
|
+
throw new ContinuationValidationError(result);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* A ready-made {@link ContinuationValidator} backed by
|
|
156
|
+
* {@link assertValidContinuation}. Drop into `AgentPromptOptions.validate`
|
|
157
|
+
* for the default prefix + tool-result-forgery + approval-forgery gate:
|
|
158
|
+
*
|
|
159
|
+
* ```ts
|
|
160
|
+
* agent.continue(id).prompt(input, { validate: defaultContinuationValidator() })
|
|
161
|
+
* ```
|
|
162
|
+
*/
|
|
163
|
+
export function defaultContinuationValidator() {
|
|
164
|
+
return (persisted, incoming, opts) => assertValidContinuation(persisted, incoming, opts);
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=continuation-validation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"continuation-validation.js","sourceRoot":"","sources":["../src/continuation-validation.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@rudderjs/ai/conversation-orm` - ORM-backed {@link ConversationStore}.
|
|
3
|
+
*
|
|
4
|
+
* Production-grade replacement for `MemoryConversationStore` (which is
|
|
5
|
+
* single-process, in-memory, and loses every thread on restart). Persists
|
|
6
|
+
* conversation threads and their messages via the registered `@rudderjs/orm`
|
|
7
|
+
* adapter - works across web processes, queue workers, and horizontally
|
|
8
|
+
* scaled deployments. Mirrors the `@rudderjs/ai/memory-orm` /
|
|
9
|
+
* `@rudderjs/ai/budget-orm` pattern.
|
|
10
|
+
*
|
|
11
|
+
* Wire it as the conversation store:
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { setConversationStore } from '@rudderjs/ai'
|
|
15
|
+
* import { OrmConversationStore } from '@rudderjs/ai/conversation-orm'
|
|
16
|
+
*
|
|
17
|
+
* setConversationStore(new OrmConversationStore())
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* The schema lives at {@link conversationOrmPrismaSchema} - copy it into your
|
|
21
|
+
* Prisma schema (or a new `prisma/schema/<file>.prisma` if you use the
|
|
22
|
+
* multi-file setup). On the native engine, add an equivalent migration; on
|
|
23
|
+
* Drizzle, define matching tables and register them via `tables: { ... }`.
|
|
24
|
+
*
|
|
25
|
+
* # Adapter coverage
|
|
26
|
+
*
|
|
27
|
+
* - Prisma - works out of the box; copy {@link conversationOrmPrismaSchema}.
|
|
28
|
+
* - Native - add a migration with the same columns.
|
|
29
|
+
* - Drizzle - define the two tables and register them on the `drizzle()`
|
|
30
|
+
* config.
|
|
31
|
+
*
|
|
32
|
+
* # Ordering & concurrency
|
|
33
|
+
*
|
|
34
|
+
* Messages carry a monotonic per-thread `position` so `load()` returns them
|
|
35
|
+
* in append order regardless of timestamp granularity. `append()` reads the
|
|
36
|
+
* current max position and assigns the next slots; like
|
|
37
|
+
* `OrmBudgetStorage.checkAndDebit`, the read-then-write is not isolated, so
|
|
38
|
+
* two concurrent appends to the SAME thread could collide on a position.
|
|
39
|
+
* Conversation threads are single-writer in practice (one user, one turn at
|
|
40
|
+
* a time), so this is a non-issue for typical apps. File an issue if you hit
|
|
41
|
+
* it; strict ordering needs a serializable transaction or a DB sequence.
|
|
42
|
+
*/
|
|
43
|
+
import { Model } from '@rudderjs/orm';
|
|
44
|
+
import type { AiMessage, ConversationStore, ConversationStoreListEntry, ConversationStoreMeta } from '../types.js';
|
|
45
|
+
/**
|
|
46
|
+
* The thread row backing {@link OrmConversationStore}. Exposed so apps that
|
|
47
|
+
* want their own queries (admin views, analytics) can use
|
|
48
|
+
* `AiConversationRecord.where(...).get()` directly.
|
|
49
|
+
*
|
|
50
|
+
* `userId` / `agent` mirror {@link ConversationStoreMeta} - `userId` scopes
|
|
51
|
+
* `list()`, `agent` carries the thread-segregation key the auto-persist
|
|
52
|
+
* machinery uses to keep one user's threads per agent class apart.
|
|
53
|
+
*/
|
|
54
|
+
export declare class AiConversationRecord extends Model {
|
|
55
|
+
static table: string;
|
|
56
|
+
static fillable: string[];
|
|
57
|
+
id: string;
|
|
58
|
+
title: string;
|
|
59
|
+
userId: string | null;
|
|
60
|
+
agent: string | null;
|
|
61
|
+
createdAt: Date;
|
|
62
|
+
updatedAt: Date | null;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* One message row in a thread. `content` and `toolCalls` are JSON-encoded
|
|
66
|
+
* strings (so a `string` content and a `ContentPart[]` content both
|
|
67
|
+
* round-trip through a portable `text` column); `position` orders them.
|
|
68
|
+
*/
|
|
69
|
+
export declare class AiConversationMessageRecord extends Model {
|
|
70
|
+
static table: string;
|
|
71
|
+
static fillable: string[];
|
|
72
|
+
id: string;
|
|
73
|
+
conversationId: string;
|
|
74
|
+
position: number;
|
|
75
|
+
role: string;
|
|
76
|
+
/** JSON-encoded `string | ContentPart[]`. */
|
|
77
|
+
content: string;
|
|
78
|
+
toolCallId: string | null;
|
|
79
|
+
/** JSON-encoded `ToolCall[]` or null. */
|
|
80
|
+
toolCalls: string | null;
|
|
81
|
+
createdAt: Date;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* {@link ConversationStore} implementation that persists rows to the
|
|
85
|
+
* registered ORM adapter. Designed for production use - the in-process
|
|
86
|
+
* `MemoryConversationStore` is for tests and dev.
|
|
87
|
+
*/
|
|
88
|
+
export declare class OrmConversationStore implements ConversationStore {
|
|
89
|
+
create(title?: string, meta?: ConversationStoreMeta): Promise<string>;
|
|
90
|
+
load(conversationId: string): Promise<AiMessage[]>;
|
|
91
|
+
append(conversationId: string, messages: AiMessage[]): Promise<void>;
|
|
92
|
+
setTitle(conversationId: string, title: string): Promise<void>;
|
|
93
|
+
list(userId?: string): Promise<ConversationStoreListEntry[]>;
|
|
94
|
+
delete(conversationId: string): Promise<void>;
|
|
95
|
+
/** Throw the same not-found error shape as `MemoryConversationStore`. */
|
|
96
|
+
private requireThread;
|
|
97
|
+
/** Next monotonic position for the thread (0 when empty). */
|
|
98
|
+
private nextPosition;
|
|
99
|
+
}
|
|
100
|
+
/** Convenience factory mirroring `ormBudgetStorage()` / `OrmUserMemory`. */
|
|
101
|
+
export declare function ormConversationStore(): OrmConversationStore;
|
|
102
|
+
/**
|
|
103
|
+
* Reference Prisma schema for `OrmConversationStore`. Copy into your
|
|
104
|
+
* `prisma/schema/<file>.prisma`. SQLite stores the `text` content as TEXT;
|
|
105
|
+
* Postgres as `text`. The `@@index` keeps `list()` (by user) and `load()`
|
|
106
|
+
* (by thread, ordered) cheap.
|
|
107
|
+
*/
|
|
108
|
+
export declare const conversationOrmPrismaSchema = "model AiConversation {\n id String @id @default(cuid())\n title String\n userId String?\n /// Thread-segregation key - the agent class name by default\n agent String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([userId])\n}\n\nmodel AiConversationMessage {\n id String @id @default(cuid())\n conversationId String\n /// Monotonic per-thread ordering\n position Int\n role String\n /// JSON-encoded `string | ContentPart[]`\n content String\n toolCallId String?\n /// JSON-encoded `ToolCall[]` or null\n toolCalls String?\n createdAt DateTime @default(now())\n\n @@index([conversationId, position])\n}\n";
|
|
109
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/conversation-orm/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAA;AACrC,OAAO,KAAK,EACV,SAAS,EACT,iBAAiB,EACjB,0BAA0B,EAC1B,qBAAqB,EAEtB,MAAM,aAAa,CAAA;AAIpB;;;;;;;;GAQG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,OAAgB,KAAK,SAAsB;IAC3C,OAAgB,QAAQ,WAA4C;IAE5D,EAAE,EAAS,MAAM,CAAA;IACjB,KAAK,EAAM,MAAM,CAAA;IACjB,MAAM,EAAK,MAAM,GAAG,IAAI,CAAA;IACxB,KAAK,EAAM,MAAM,GAAG,IAAI,CAAA;IACxB,SAAS,EAAE,IAAI,CAAA;IACf,SAAS,EAAE,IAAI,GAAG,IAAI,CAAA;CAC/B;AAED;;;;GAIG;AACH,qBAAa,2BAA4B,SAAQ,KAAK;IACpD,OAAgB,KAAK,SAA6B;IAClD,OAAgB,QAAQ,WAA+E;IAE/F,EAAE,EAAc,MAAM,CAAA;IACtB,cAAc,EAAE,MAAM,CAAA;IACtB,QAAQ,EAAQ,MAAM,CAAA;IACtB,IAAI,EAAY,MAAM,CAAA;IAC9B,6CAA6C;IACrC,OAAO,EAAS,MAAM,CAAA;IACtB,UAAU,EAAM,MAAM,GAAG,IAAI,CAAA;IACrC,yCAAyC;IACjC,SAAS,EAAO,MAAM,GAAG,IAAI,CAAA;IAC7B,SAAS,EAAO,IAAI,CAAA;CAC7B;AAID;;;;GAIG;AACH,qBAAa,oBAAqB,YAAW,iBAAiB;IACtD,MAAM,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,MAAM,CAAC;IASrE,IAAI,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IASlD,MAAM,CAAC,cAAc,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAapE,QAAQ,CAAC,cAAc,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAO9D,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,0BAA0B,EAAE,CAAC;IAO5D,MAAM,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKnD,yEAAyE;YAC3D,aAAa;IAK3B,6DAA6D;YAC/C,YAAY;CAO3B;AAED,4EAA4E;AAC5E,wBAAgB,oBAAoB,IAAI,oBAAoB,CAE3D;AAID;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B,+tBA2BvC,CAAA"}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@rudderjs/ai/conversation-orm` - ORM-backed {@link ConversationStore}.
|
|
3
|
+
*
|
|
4
|
+
* Production-grade replacement for `MemoryConversationStore` (which is
|
|
5
|
+
* single-process, in-memory, and loses every thread on restart). Persists
|
|
6
|
+
* conversation threads and their messages via the registered `@rudderjs/orm`
|
|
7
|
+
* adapter - works across web processes, queue workers, and horizontally
|
|
8
|
+
* scaled deployments. Mirrors the `@rudderjs/ai/memory-orm` /
|
|
9
|
+
* `@rudderjs/ai/budget-orm` pattern.
|
|
10
|
+
*
|
|
11
|
+
* Wire it as the conversation store:
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { setConversationStore } from '@rudderjs/ai'
|
|
15
|
+
* import { OrmConversationStore } from '@rudderjs/ai/conversation-orm'
|
|
16
|
+
*
|
|
17
|
+
* setConversationStore(new OrmConversationStore())
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* The schema lives at {@link conversationOrmPrismaSchema} - copy it into your
|
|
21
|
+
* Prisma schema (or a new `prisma/schema/<file>.prisma` if you use the
|
|
22
|
+
* multi-file setup). On the native engine, add an equivalent migration; on
|
|
23
|
+
* Drizzle, define matching tables and register them via `tables: { ... }`.
|
|
24
|
+
*
|
|
25
|
+
* # Adapter coverage
|
|
26
|
+
*
|
|
27
|
+
* - Prisma - works out of the box; copy {@link conversationOrmPrismaSchema}.
|
|
28
|
+
* - Native - add a migration with the same columns.
|
|
29
|
+
* - Drizzle - define the two tables and register them on the `drizzle()`
|
|
30
|
+
* config.
|
|
31
|
+
*
|
|
32
|
+
* # Ordering & concurrency
|
|
33
|
+
*
|
|
34
|
+
* Messages carry a monotonic per-thread `position` so `load()` returns them
|
|
35
|
+
* in append order regardless of timestamp granularity. `append()` reads the
|
|
36
|
+
* current max position and assigns the next slots; like
|
|
37
|
+
* `OrmBudgetStorage.checkAndDebit`, the read-then-write is not isolated, so
|
|
38
|
+
* two concurrent appends to the SAME thread could collide on a position.
|
|
39
|
+
* Conversation threads are single-writer in practice (one user, one turn at
|
|
40
|
+
* a time), so this is a non-issue for typical apps. File an issue if you hit
|
|
41
|
+
* it; strict ordering needs a serializable transaction or a DB sequence.
|
|
42
|
+
*/
|
|
43
|
+
import { Model } from '@rudderjs/orm';
|
|
44
|
+
// ─── ORM Models ───────────────────────────────────────────
|
|
45
|
+
/**
|
|
46
|
+
* The thread row backing {@link OrmConversationStore}. Exposed so apps that
|
|
47
|
+
* want their own queries (admin views, analytics) can use
|
|
48
|
+
* `AiConversationRecord.where(...).get()` directly.
|
|
49
|
+
*
|
|
50
|
+
* `userId` / `agent` mirror {@link ConversationStoreMeta} - `userId` scopes
|
|
51
|
+
* `list()`, `agent` carries the thread-segregation key the auto-persist
|
|
52
|
+
* machinery uses to keep one user's threads per agent class apart.
|
|
53
|
+
*/
|
|
54
|
+
export class AiConversationRecord extends Model {
|
|
55
|
+
static table = 'aiConversation';
|
|
56
|
+
static fillable = ['title', 'userId', 'agent', 'updatedAt'];
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* One message row in a thread. `content` and `toolCalls` are JSON-encoded
|
|
60
|
+
* strings (so a `string` content and a `ContentPart[]` content both
|
|
61
|
+
* round-trip through a portable `text` column); `position` orders them.
|
|
62
|
+
*/
|
|
63
|
+
export class AiConversationMessageRecord extends Model {
|
|
64
|
+
static table = 'aiConversationMessage';
|
|
65
|
+
static fillable = ['conversationId', 'position', 'role', 'content', 'toolCallId', 'toolCalls'];
|
|
66
|
+
}
|
|
67
|
+
// ─── ConversationStore adapter ────────────────────────────
|
|
68
|
+
/**
|
|
69
|
+
* {@link ConversationStore} implementation that persists rows to the
|
|
70
|
+
* registered ORM adapter. Designed for production use - the in-process
|
|
71
|
+
* `MemoryConversationStore` is for tests and dev.
|
|
72
|
+
*/
|
|
73
|
+
export class OrmConversationStore {
|
|
74
|
+
async create(title, meta) {
|
|
75
|
+
const data = { title: title ?? 'New conversation' };
|
|
76
|
+
if (meta?.userId !== undefined)
|
|
77
|
+
data['userId'] = meta.userId;
|
|
78
|
+
if (meta?.agent !== undefined)
|
|
79
|
+
data['agent'] = meta.agent;
|
|
80
|
+
const created = await AiConversationRecord.create(data);
|
|
81
|
+
return created.id;
|
|
82
|
+
}
|
|
83
|
+
async load(conversationId) {
|
|
84
|
+
await this.requireThread(conversationId);
|
|
85
|
+
const rows = await AiConversationMessageRecord
|
|
86
|
+
.where('conversationId', conversationId)
|
|
87
|
+
.orderBy('position', 'ASC')
|
|
88
|
+
.get();
|
|
89
|
+
return rows.map(rowToMessage);
|
|
90
|
+
}
|
|
91
|
+
async append(conversationId, messages) {
|
|
92
|
+
await this.requireThread(conversationId);
|
|
93
|
+
if (messages.length === 0)
|
|
94
|
+
return;
|
|
95
|
+
let position = await this.nextPosition(conversationId);
|
|
96
|
+
for (const message of messages) {
|
|
97
|
+
await AiConversationMessageRecord.create(messageToRow(conversationId, position, message));
|
|
98
|
+
position++;
|
|
99
|
+
}
|
|
100
|
+
await AiConversationRecord.where('id', conversationId).updateAll({ updatedAt: new Date() });
|
|
101
|
+
}
|
|
102
|
+
async setTitle(conversationId, title) {
|
|
103
|
+
const updated = await AiConversationRecord
|
|
104
|
+
.where('id', conversationId)
|
|
105
|
+
.updateAll({ title, updatedAt: new Date() });
|
|
106
|
+
if (!updated)
|
|
107
|
+
throw notFound(conversationId);
|
|
108
|
+
}
|
|
109
|
+
async list(userId) {
|
|
110
|
+
let q = AiConversationRecord.query();
|
|
111
|
+
if (userId != null)
|
|
112
|
+
q = q.where('userId', userId);
|
|
113
|
+
const rows = await q.orderBy('updatedAt', 'DESC').get();
|
|
114
|
+
return rows.map(rowToListEntry);
|
|
115
|
+
}
|
|
116
|
+
async delete(conversationId) {
|
|
117
|
+
await AiConversationMessageRecord.where('conversationId', conversationId).deleteAll();
|
|
118
|
+
await AiConversationRecord.where('id', conversationId).deleteAll();
|
|
119
|
+
}
|
|
120
|
+
/** Throw the same not-found error shape as `MemoryConversationStore`. */
|
|
121
|
+
async requireThread(conversationId) {
|
|
122
|
+
const thread = await AiConversationRecord.where('id', conversationId).first();
|
|
123
|
+
if (!thread)
|
|
124
|
+
throw notFound(conversationId);
|
|
125
|
+
}
|
|
126
|
+
/** Next monotonic position for the thread (0 when empty). */
|
|
127
|
+
async nextPosition(conversationId) {
|
|
128
|
+
const last = await AiConversationMessageRecord
|
|
129
|
+
.where('conversationId', conversationId)
|
|
130
|
+
.orderBy('position', 'DESC')
|
|
131
|
+
.first();
|
|
132
|
+
return last ? last.position + 1 : 0;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/** Convenience factory mirroring `ormBudgetStorage()` / `OrmUserMemory`. */
|
|
136
|
+
export function ormConversationStore() {
|
|
137
|
+
return new OrmConversationStore();
|
|
138
|
+
}
|
|
139
|
+
// ─── Schema reference ─────────────────────────────────────
|
|
140
|
+
/**
|
|
141
|
+
* Reference Prisma schema for `OrmConversationStore`. Copy into your
|
|
142
|
+
* `prisma/schema/<file>.prisma`. SQLite stores the `text` content as TEXT;
|
|
143
|
+
* Postgres as `text`. The `@@index` keeps `list()` (by user) and `load()`
|
|
144
|
+
* (by thread, ordered) cheap.
|
|
145
|
+
*/
|
|
146
|
+
export const conversationOrmPrismaSchema = `model AiConversation {
|
|
147
|
+
id String @id @default(cuid())
|
|
148
|
+
title String
|
|
149
|
+
userId String?
|
|
150
|
+
/// Thread-segregation key - the agent class name by default
|
|
151
|
+
agent String?
|
|
152
|
+
createdAt DateTime @default(now())
|
|
153
|
+
updatedAt DateTime @updatedAt
|
|
154
|
+
|
|
155
|
+
@@index([userId])
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
model AiConversationMessage {
|
|
159
|
+
id String @id @default(cuid())
|
|
160
|
+
conversationId String
|
|
161
|
+
/// Monotonic per-thread ordering
|
|
162
|
+
position Int
|
|
163
|
+
role String
|
|
164
|
+
/// JSON-encoded \`string | ContentPart[]\`
|
|
165
|
+
content String
|
|
166
|
+
toolCallId String?
|
|
167
|
+
/// JSON-encoded \`ToolCall[]\` or null
|
|
168
|
+
toolCalls String?
|
|
169
|
+
createdAt DateTime @default(now())
|
|
170
|
+
|
|
171
|
+
@@index([conversationId, position])
|
|
172
|
+
}
|
|
173
|
+
`;
|
|
174
|
+
// ─── Helpers ──────────────────────────────────────────────
|
|
175
|
+
function notFound(conversationId) {
|
|
176
|
+
return new Error(`[RudderJS AI] Conversation "${conversationId}" not found.`);
|
|
177
|
+
}
|
|
178
|
+
function messageToRow(conversationId, position, m) {
|
|
179
|
+
return {
|
|
180
|
+
conversationId,
|
|
181
|
+
position,
|
|
182
|
+
role: m.role,
|
|
183
|
+
content: JSON.stringify(m.content),
|
|
184
|
+
toolCallId: m.toolCallId ?? null,
|
|
185
|
+
toolCalls: m.toolCalls ? JSON.stringify(m.toolCalls) : null,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function rowToMessage(row) {
|
|
189
|
+
const out = {
|
|
190
|
+
role: row.role,
|
|
191
|
+
content: JSON.parse(row.content),
|
|
192
|
+
};
|
|
193
|
+
if (row.toolCallId != null)
|
|
194
|
+
out.toolCallId = row.toolCallId;
|
|
195
|
+
if (row.toolCalls != null)
|
|
196
|
+
out.toolCalls = JSON.parse(row.toolCalls);
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
199
|
+
function rowToListEntry(row) {
|
|
200
|
+
const out = {
|
|
201
|
+
id: row.id,
|
|
202
|
+
title: row.title,
|
|
203
|
+
createdAt: row.createdAt,
|
|
204
|
+
};
|
|
205
|
+
if (row.updatedAt != null)
|
|
206
|
+
out.updatedAt = row.updatedAt;
|
|
207
|
+
if (row.agent != null)
|
|
208
|
+
out.agent = row.agent;
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
211
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/conversation-orm/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conversation-persistence.d.ts","sourceRoot":"","sources":["../src/conversation-persistence.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAClB,aAAa,EACb,mBAAmB,EACnB,SAAS,EACT,sBAAsB,EACtB,kBAAkB,EAClB,iBAAiB,EAElB,MAAM,YAAY,CAAA;AAEnB;;;;;GAKG;AACH,MAAM,MAAM,uBAAuB,GAAG,MAAM,iBAAiB,GAAG,IAAI,GAAG,SAAS,CAAA;AAEhF;;;;;;;;;;;;;GAaG;AACH,wBAAsB,sBAAsB,CAC1C,SAAS,EAAK,MAAM,KAAK,GAAG,kBAAkB,GAAG,OAAO,CAAC,KAAK,GAAG,kBAAkB,CAAC,EACpF,OAAO,EAAO,sBAAsB,GAAG,SAAS,GAC/C,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAWpC;
|
|
1
|
+
{"version":3,"file":"conversation-persistence.d.ts","sourceRoot":"","sources":["../src/conversation-persistence.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAClB,aAAa,EACb,mBAAmB,EACnB,SAAS,EACT,sBAAsB,EACtB,kBAAkB,EAClB,iBAAiB,EAElB,MAAM,YAAY,CAAA;AAEnB;;;;;GAKG;AACH,MAAM,MAAM,uBAAuB,GAAG,MAAM,iBAAiB,GAAG,IAAI,GAAG,SAAS,CAAA;AAEhF;;;;;;;;;;;;;GAaG;AACH,wBAAsB,sBAAsB,CAC1C,SAAS,EAAK,MAAM,KAAK,GAAG,kBAAkB,GAAG,OAAO,CAAC,KAAK,GAAG,kBAAkB,CAAC,EACpF,OAAO,EAAO,sBAAsB,GAAG,SAAS,GAC/C,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAWpC;AA0FD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,GAAG,SAAS,EAAE,CAUvF;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,EAAY,kBAAkB,EAClC,cAAc,EAAE,MAAM,EACtB,WAAW,EAAK,uBAAuB,EACvC,KAAK,EAAW,MAAM,EACtB,OAAO,EAAS,kBAAkB,GAAG,SAAS,EAC9C,KAAK,EAAW,CAAC,UAAU,EAAE,kBAAkB,GAAG,SAAS,KAAK,OAAO,CAAC,aAAa,CAAC,GACrF,OAAO,CAAC,aAAa,CAAC,CAWxB;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,CACzC,IAAI,EAAY,kBAAkB,EAClC,cAAc,EAAE,MAAM,EACtB,WAAW,EAAK,uBAAuB,EACvC,KAAK,EAAW,MAAM,EACtB,OAAO,EAAS,kBAAkB,GAAG,SAAS,EAC9C,KAAK,EAAW,CAAC,UAAU,EAAE,kBAAkB,GAAG,SAAS,KAAK,mBAAmB,GAClF,mBAAmB,CAyCrB"}
|
|
@@ -63,11 +63,33 @@ async function preparePersistence(spec, agentClassName, store, callerHistory) {
|
|
|
63
63
|
else {
|
|
64
64
|
throw new Error('[RudderJS AI] ConversationalSpec must include either `user` or `id`.');
|
|
65
65
|
}
|
|
66
|
+
// Snapshot the trusted baseline before any limit slice — the validation
|
|
67
|
+
// hook compares the caller's incoming messages against the FULL persisted
|
|
68
|
+
// thread, not the windowed view fed to the model.
|
|
69
|
+
const persisted = loaded;
|
|
70
|
+
let windowed = loaded;
|
|
66
71
|
if (spec.historyLimit !== undefined && spec.historyLimit > 0) {
|
|
67
|
-
|
|
72
|
+
windowed = loaded.slice(-spec.historyLimit);
|
|
68
73
|
}
|
|
69
|
-
const history = [...
|
|
70
|
-
return { spec, store, convId, history };
|
|
74
|
+
const history = [...windowed, ...(callerHistory ?? [])];
|
|
75
|
+
return { spec, store, convId, history, persisted };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Run the caller-supplied `validate` continuation hook, if present. The
|
|
79
|
+
* "incoming" view is the caller's claimed prior conversation — their
|
|
80
|
+
* `options.messages` (full continuation list) when set, else
|
|
81
|
+
* `options.history`. Throws (propagating the rejection) when the hook does.
|
|
82
|
+
*/
|
|
83
|
+
async function runValidation(ctx, options) {
|
|
84
|
+
if (!options?.validate)
|
|
85
|
+
return;
|
|
86
|
+
const incoming = options.messages ?? options.history ?? [];
|
|
87
|
+
const opts = {};
|
|
88
|
+
if (options.approvedToolCallIds)
|
|
89
|
+
opts.approvedToolCallIds = options.approvedToolCallIds;
|
|
90
|
+
if (options.rejectedToolCallIds)
|
|
91
|
+
opts.rejectedToolCallIds = options.rejectedToolCallIds;
|
|
92
|
+
await options.validate(ctx.persisted, incoming, opts);
|
|
71
93
|
}
|
|
72
94
|
/**
|
|
73
95
|
* Compose the new turn's messages from the user input + the steps the
|
|
@@ -98,6 +120,7 @@ export async function runWithPersistence(spec, agentClassName, storeLookup, inpu
|
|
|
98
120
|
if (!store)
|
|
99
121
|
throw new Error('[RudderJS AI] No ConversationStore registered. Bind one via `setConversationStore()` or the `ai.conversations` DI key.');
|
|
100
122
|
const ctx = await preparePersistence(spec, agentClassName, store, options?.history);
|
|
123
|
+
await runValidation(ctx, options);
|
|
101
124
|
const effOptions = { ...options, history: ctx.history };
|
|
102
125
|
const response = await inner(effOptions);
|
|
103
126
|
await store.append(ctx.convId, newMessagesFromTurn(input, response));
|
|
@@ -123,6 +146,7 @@ export function runWithPersistenceStreaming(spec, agentClassName, storeLookup, i
|
|
|
123
146
|
let ctx;
|
|
124
147
|
try {
|
|
125
148
|
ctx = await preparePersistence(spec, agentClassName, store, options?.history);
|
|
149
|
+
await runValidation(ctx, options);
|
|
126
150
|
}
|
|
127
151
|
catch (err) {
|
|
128
152
|
rejectResponse(err);
|