@veryfront/ext-redis 0.1.1186 → 0.1.1190
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/README.md +12 -0
- package/esm/_dnt.polyfills.d.ts +12 -0
- package/esm/_dnt.polyfills.d.ts.map +1 -0
- package/esm/_dnt.polyfills.js +15 -0
- package/esm/_dnt.shims.d.ts +11 -0
- package/esm/_dnt.shims.d.ts.map +1 -0
- package/esm/_dnt.shims.js +68 -0
- package/esm/agent-memory.d.ts +60 -0
- package/esm/agent-memory.d.ts.map +1 -0
- package/esm/agent-memory.js +173 -0
- package/esm/cache-administration.d.ts +5 -0
- package/esm/cache-administration.d.ts.map +1 -0
- package/esm/cache-administration.js +113 -0
- package/esm/cache-backend.d.ts +46 -0
- package/esm/cache-backend.d.ts.map +1 -0
- package/esm/cache-backend.js +778 -0
- package/esm/index.d.ts +7 -0
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +7 -0
- package/esm/rate-limit-store.d.ts +40 -0
- package/esm/rate-limit-store.d.ts.map +1 -0
- package/esm/rate-limit-store.js +306 -0
- package/esm/redis-client-manager.d.ts +8 -0
- package/esm/redis-client-manager.d.ts.map +1 -1
- package/esm/redis-client-manager.js +22 -1
- package/esm/redis-runtime-provider.js +1 -1
- package/esm/render-cache-store.d.ts +34 -0
- package/esm/render-cache-store.d.ts.map +1 -0
- package/esm/render-cache-store.js +191 -0
- package/esm/revisioned-cache-record.d.ts +18 -0
- package/esm/revisioned-cache-record.d.ts.map +1 -0
- package/esm/revisioned-cache-record.js +68 -0
- package/esm/routing-invalidation-bus.d.ts +33 -0
- package/esm/routing-invalidation-bus.d.ts.map +1 -0
- package/esm/routing-invalidation-bus.js +760 -0
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -32,6 +32,18 @@ Core Redis facades fail closed with an actionable install recommendation until
|
|
|
32
32
|
extension orchestration registers the provider. Workspace and compiled-binary
|
|
33
33
|
availability do not bypass that activation boundary.
|
|
34
34
|
|
|
35
|
+
## Redis-backed infrastructure primitives
|
|
36
|
+
|
|
37
|
+
The package also exports bounded Redis implementations for distributed cache,
|
|
38
|
+
render-cache, rate-limit, routing-invalidation, and agent-memory composition.
|
|
39
|
+
These primitives are explicit: constructing one does not activate Redis for
|
|
40
|
+
unrelated framework features, and each owner must close or destroy the
|
|
41
|
+
resource it creates.
|
|
42
|
+
|
|
43
|
+
Durable workflow storage is intentionally not part of this release. It will be
|
|
44
|
+
published after the provider-neutral workflow recovery contracts land, rather
|
|
45
|
+
than coupling this package to unpublished core APIs.
|
|
46
|
+
|
|
35
47
|
## Configuration
|
|
36
48
|
|
|
37
49
|
The shared client facade reads `REDIS_URL` when an explicit URL is not passed.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
declare global {
|
|
2
|
+
interface Object {
|
|
3
|
+
/**
|
|
4
|
+
* Determines whether an object has a property with the specified name.
|
|
5
|
+
* @param o An object.
|
|
6
|
+
* @param v A property name.
|
|
7
|
+
*/
|
|
8
|
+
hasOwn(o: object, v: PropertyKey): boolean;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=_dnt.polyfills.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_dnt.polyfills.d.ts","sourceRoot":"","sources":["../src/_dnt.polyfills.ts"],"names":[],"mappings":"AAeA,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd;;;;WAIG;QACH,MAAM,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC;KAC5C;CACF;AAED,OAAO,EAAE,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// https://github.com/tc39/proposal-accessible-object-hasownproperty/blob/main/polyfill.js
|
|
2
|
+
if (!Object.hasOwn) {
|
|
3
|
+
Object.defineProperty(Object, "hasOwn", {
|
|
4
|
+
value: function (object, property) {
|
|
5
|
+
if (object == null) {
|
|
6
|
+
throw new TypeError("Cannot convert undefined or null to object");
|
|
7
|
+
}
|
|
8
|
+
return Object.prototype.hasOwnProperty.call(Object(object), property);
|
|
9
|
+
},
|
|
10
|
+
configurable: true,
|
|
11
|
+
enumerable: false,
|
|
12
|
+
writable: true,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Deno } from "@deno/shim-deno";
|
|
2
|
+
export { Deno } from "@deno/shim-deno";
|
|
3
|
+
export { crypto, type Crypto, type SubtleCrypto, type AlgorithmIdentifier, type Algorithm, type RsaOaepParams, type BufferSource, type AesCtrParams, type AesCbcParams, type AesGcmParams, type CryptoKey, type KeyAlgorithm, type KeyType, type KeyUsage, type EcdhKeyDeriveParams, type HkdfParams, type HashAlgorithmIdentifier, type Pbkdf2Params, type AesDerivedKeyParams, type HmacImportParams, type JsonWebKey, type RsaOtherPrimesInfo, type KeyFormat, type RsaHashedKeyGenParams, type RsaKeyGenParams, type BigInteger, type EcKeyGenParams, type NamedCurve, type CryptoKeyPair, type AesKeyGenParams, type HmacKeyGenParams, type RsaHashedImportParams, type EcKeyImportParams, type AesKeyAlgorithm, type RsaPssParams, type EcdsaParams } from "@deno/shim-crypto";
|
|
4
|
+
export { setInterval, setTimeout } from "@deno/shim-timers";
|
|
5
|
+
export declare const dntGlobalThis: Omit<typeof globalThis, "Deno" | "crypto" | "setInterval" | "setTimeout"> & {
|
|
6
|
+
Deno: typeof Deno;
|
|
7
|
+
crypto: import("@deno/shim-crypto").Crypto;
|
|
8
|
+
setInterval: any;
|
|
9
|
+
setTimeout: any;
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=_dnt.shims.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_dnt.shims.d.ts","sourceRoot":"","sources":["../src/_dnt.shims.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AACvC,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAEvC,OAAO,EAAE,MAAM,EAAE,KAAK,MAAM,EAAE,KAAK,YAAY,EAAE,KAAK,mBAAmB,EAAE,KAAK,SAAS,EAAE,KAAK,aAAa,EAAE,KAAK,YAAY,EAAE,KAAK,YAAY,EAAE,KAAK,YAAY,EAAE,KAAK,YAAY,EAAE,KAAK,SAAS,EAAE,KAAK,YAAY,EAAE,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,KAAK,mBAAmB,EAAE,KAAK,UAAU,EAAE,KAAK,uBAAuB,EAAE,KAAK,YAAY,EAAE,KAAK,mBAAmB,EAAE,KAAK,gBAAgB,EAAE,KAAK,UAAU,EAAE,KAAK,kBAAkB,EAAE,KAAK,SAAS,EAAE,KAAK,qBAAqB,EAAE,KAAK,eAAe,EAAE,KAAK,UAAU,EAAE,KAAK,cAAc,EAAE,KAAK,UAAU,EAAE,KAAK,aAAa,EAAE,KAAK,eAAe,EAAE,KAAK,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,KAAK,iBAAiB,EAAE,KAAK,eAAe,EAAE,KAAK,YAAY,EAAE,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErvB,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAQ5D,eAAO,MAAM,aAAa;;;;;CAA2C,CAAC"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Deno } from "@deno/shim-deno";
|
|
2
|
+
export { Deno } from "@deno/shim-deno";
|
|
3
|
+
import { crypto } from "@deno/shim-crypto";
|
|
4
|
+
export { crypto } from "@deno/shim-crypto";
|
|
5
|
+
import { setInterval, setTimeout } from "@deno/shim-timers";
|
|
6
|
+
export { setInterval, setTimeout } from "@deno/shim-timers";
|
|
7
|
+
const dntGlobals = {
|
|
8
|
+
Deno,
|
|
9
|
+
crypto,
|
|
10
|
+
setInterval,
|
|
11
|
+
setTimeout,
|
|
12
|
+
};
|
|
13
|
+
export const dntGlobalThis = createMergeProxy(globalThis, dntGlobals);
|
|
14
|
+
function createMergeProxy(baseObj, extObj) {
|
|
15
|
+
return new Proxy(baseObj, {
|
|
16
|
+
get(_target, prop, _receiver) {
|
|
17
|
+
if (prop in extObj) {
|
|
18
|
+
return extObj[prop];
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
return baseObj[prop];
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
set(_target, prop, value) {
|
|
25
|
+
if (prop in extObj) {
|
|
26
|
+
delete extObj[prop];
|
|
27
|
+
}
|
|
28
|
+
baseObj[prop] = value;
|
|
29
|
+
return true;
|
|
30
|
+
},
|
|
31
|
+
deleteProperty(_target, prop) {
|
|
32
|
+
let success = false;
|
|
33
|
+
if (prop in extObj) {
|
|
34
|
+
delete extObj[prop];
|
|
35
|
+
success = true;
|
|
36
|
+
}
|
|
37
|
+
if (prop in baseObj) {
|
|
38
|
+
delete baseObj[prop];
|
|
39
|
+
success = true;
|
|
40
|
+
}
|
|
41
|
+
return success;
|
|
42
|
+
},
|
|
43
|
+
ownKeys(_target) {
|
|
44
|
+
const baseKeys = Reflect.ownKeys(baseObj);
|
|
45
|
+
const extKeys = Reflect.ownKeys(extObj);
|
|
46
|
+
const extKeysSet = new Set(extKeys);
|
|
47
|
+
return [...baseKeys.filter((k) => !extKeysSet.has(k)), ...extKeys];
|
|
48
|
+
},
|
|
49
|
+
defineProperty(_target, prop, desc) {
|
|
50
|
+
if (prop in extObj) {
|
|
51
|
+
delete extObj[prop];
|
|
52
|
+
}
|
|
53
|
+
Reflect.defineProperty(baseObj, prop, desc);
|
|
54
|
+
return true;
|
|
55
|
+
},
|
|
56
|
+
getOwnPropertyDescriptor(_target, prop) {
|
|
57
|
+
if (prop in extObj) {
|
|
58
|
+
return Reflect.getOwnPropertyDescriptor(extObj, prop);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
return Reflect.getOwnPropertyDescriptor(baseObj, prop);
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
has(_target, prop) {
|
|
65
|
+
return prop in extObj || prop in baseObj;
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/****
|
|
2
|
+
* Redis Memory Backend
|
|
3
|
+
*
|
|
4
|
+
* Distributed memory implementation using Redis for multi-process deployments.
|
|
5
|
+
* Enables conversation persistence across server restarts and horizontal scaling.
|
|
6
|
+
*/
|
|
7
|
+
import { type Memory, type MemoryConfigBase, type MemoryStats, type MinimalMessage } from "veryfront/extensions/distributed/agent-memory-support";
|
|
8
|
+
/**
|
|
9
|
+
* Redis client interface (compatible with ioredis and node-redis)
|
|
10
|
+
*/
|
|
11
|
+
export interface RedisEvalOptions {
|
|
12
|
+
keys: string[];
|
|
13
|
+
arguments: string[];
|
|
14
|
+
}
|
|
15
|
+
export interface RedisClient {
|
|
16
|
+
get(key: string): Promise<string | null>;
|
|
17
|
+
set(key: string, value: string, options?: {
|
|
18
|
+
EX?: number;
|
|
19
|
+
}): Promise<unknown>;
|
|
20
|
+
del(key: string): Promise<number>;
|
|
21
|
+
expire(key: string, seconds: number): Promise<number>;
|
|
22
|
+
eval?(script: string, options: RedisEvalOptions): Promise<unknown>;
|
|
23
|
+
sendCommand?(args: string[]): Promise<unknown>;
|
|
24
|
+
call?(command: string, ...args: string[]): Promise<unknown>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Redis memory configuration
|
|
28
|
+
*/
|
|
29
|
+
export interface RedisMemoryConfig extends MemoryConfigBase {
|
|
30
|
+
type: "redis";
|
|
31
|
+
/** Redis client instance */
|
|
32
|
+
client: RedisClient;
|
|
33
|
+
/** Key prefix for namespacing */
|
|
34
|
+
keyPrefix?: string;
|
|
35
|
+
/** User ID for per-user memory isolation */
|
|
36
|
+
userId?: string;
|
|
37
|
+
/** TTL in seconds (default: 24 hours) */
|
|
38
|
+
ttl?: number;
|
|
39
|
+
}
|
|
40
|
+
/** Implement redis memory. */
|
|
41
|
+
export declare class RedisMemory<M extends MinimalMessage = MinimalMessage> implements Memory<M> {
|
|
42
|
+
private client;
|
|
43
|
+
private agentId;
|
|
44
|
+
private userId;
|
|
45
|
+
private keyPrefix;
|
|
46
|
+
private ttl;
|
|
47
|
+
private config;
|
|
48
|
+
constructor(agentId: string, config: RedisMemoryConfig);
|
|
49
|
+
private getKey;
|
|
50
|
+
private parseMessages;
|
|
51
|
+
add(message: M): Promise<void>;
|
|
52
|
+
getMessages(): Promise<M[]>;
|
|
53
|
+
clear(): Promise<void>;
|
|
54
|
+
getStats(): Promise<MemoryStats>;
|
|
55
|
+
touch(): Promise<void>;
|
|
56
|
+
private atomicAdd;
|
|
57
|
+
}
|
|
58
|
+
/** Create redis memory. */
|
|
59
|
+
export declare function createRedisMemory<M extends MinimalMessage = MinimalMessage>(agentId: string, config: RedisMemoryConfig): RedisMemory<M>;
|
|
60
|
+
//# sourceMappingURL=agent-memory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-memory.d.ts","sourceRoot":"","sources":["../src/agent-memory.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAEL,KAAK,MAAM,EACX,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,KAAK,cAAc,EACpB,MAAM,uDAAuD,CAAC;AAI/D;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACzC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7E,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAClC,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACtD,IAAI,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACnE,WAAW,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/C,IAAI,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC7D;AAED;;GAEG;AACH,MAAM,WAAW,iBAAkB,SAAQ,gBAAgB;IACzD,IAAI,EAAE,OAAO,CAAC;IACd,4BAA4B;IAC5B,MAAM,EAAE,WAAW,CAAC;IACpB,iCAAiC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yCAAyC;IACzC,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AA4ED,8BAA8B;AAC9B,qBAAa,WAAW,CAAC,CAAC,SAAS,cAAc,GAAG,cAAc,CAAE,YAAW,MAAM,CAAC,CAAC,CAAC;IACtF,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,GAAG,CAAS;IACpB,OAAO,CAAC,MAAM,CAAoB;gBAEtB,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB;IAStD,OAAO,CAAC,MAAM;IAId,OAAO,CAAC,aAAa;IAgBrB,GAAG,CAAC,OAAO,EAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAW9B,WAAW,IAAI,OAAO,CAAC,CAAC,EAAE,CAAC;IAQ3B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAUtB,QAAQ,IAAI,OAAO,CAAC,WAAW,CAAC;IAehC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAYtB,OAAO,CAAC,SAAS;CA2BlB;AAED,2BAA2B;AAC3B,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,cAAc,GAAG,cAAc,EACzE,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,iBAAiB,GACxB,WAAW,CAAC,CAAC,CAAC,CAEhB"}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/****
|
|
2
|
+
* Redis Memory Backend
|
|
3
|
+
*
|
|
4
|
+
* Distributed memory implementation using Redis for multi-process deployments.
|
|
5
|
+
* Enables conversation persistence across server restarts and horizontal scaling.
|
|
6
|
+
*/
|
|
7
|
+
import { estimateTokens, } from "veryfront/extensions/distributed/agent-memory-support";
|
|
8
|
+
import { withSpan } from "veryfront/observability/otlp-setup";
|
|
9
|
+
import { agentLogger } from "veryfront/utils/logger";
|
|
10
|
+
const DEFAULT_TTL = 86_400; // 24 hours
|
|
11
|
+
const DEFAULT_KEY_PREFIX = "veryfront:agent:memory:";
|
|
12
|
+
const ATOMIC_ADD_SCRIPT = `
|
|
13
|
+
local key = KEYS[1]
|
|
14
|
+
local message_json = ARGV[1]
|
|
15
|
+
local max_messages = tonumber(ARGV[2]) or 0
|
|
16
|
+
local max_tokens = tonumber(ARGV[3]) or 0
|
|
17
|
+
local ttl = tonumber(ARGV[4]) or 0
|
|
18
|
+
|
|
19
|
+
local function decode_json(value, label)
|
|
20
|
+
local ok, decoded = pcall(cjson.decode, value)
|
|
21
|
+
if not ok then
|
|
22
|
+
return nil, redis.error_reply("CORRUPT_REDIS_MEMORY_JSON:" .. label)
|
|
23
|
+
end
|
|
24
|
+
if type(decoded) ~= "table" then
|
|
25
|
+
return nil, redis.error_reply("CORRUPT_REDIS_MEMORY_JSON:" .. label)
|
|
26
|
+
end
|
|
27
|
+
return decoded, nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
local messages = {}
|
|
31
|
+
local current = redis.call("GET", key)
|
|
32
|
+
if current and current ~= "" then
|
|
33
|
+
local decoded, decode_error = decode_json(current, "stored")
|
|
34
|
+
if decode_error then return decode_error end
|
|
35
|
+
messages = decoded
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
local message, message_error = decode_json(message_json, "message")
|
|
39
|
+
if message_error then return message_error end
|
|
40
|
+
table.insert(messages, message)
|
|
41
|
+
|
|
42
|
+
if max_messages > 0 then
|
|
43
|
+
while #messages > max_messages do
|
|
44
|
+
table.remove(messages, 1)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
local function message_text_length(item)
|
|
49
|
+
if type(item) ~= "table" or type(item.parts) ~= "table" then return 0 end
|
|
50
|
+
local total = 0
|
|
51
|
+
for _, part in ipairs(item.parts) do
|
|
52
|
+
if type(part) == "table" and part.type == "text" and type(part.text) == "string" then
|
|
53
|
+
total = total + string.len(part.text)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
return total
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
local function estimate_tokens(items)
|
|
60
|
+
local total = 0
|
|
61
|
+
for _, item in ipairs(items) do
|
|
62
|
+
total = total + message_text_length(item)
|
|
63
|
+
end
|
|
64
|
+
return math.ceil(total / 4)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
if max_tokens > 0 then
|
|
68
|
+
while #messages > 1 and estimate_tokens(messages) > max_tokens do
|
|
69
|
+
table.remove(messages, 1)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
local encoded = cjson.encode(messages)
|
|
74
|
+
if ttl > 0 then
|
|
75
|
+
redis.call("SET", key, encoded, "EX", ttl)
|
|
76
|
+
else
|
|
77
|
+
redis.call("SET", key, encoded)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
return #messages
|
|
81
|
+
`;
|
|
82
|
+
/** Implement redis memory. */
|
|
83
|
+
export class RedisMemory {
|
|
84
|
+
client;
|
|
85
|
+
agentId;
|
|
86
|
+
userId;
|
|
87
|
+
keyPrefix;
|
|
88
|
+
ttl;
|
|
89
|
+
config;
|
|
90
|
+
constructor(agentId, config) {
|
|
91
|
+
this.client = config.client;
|
|
92
|
+
this.agentId = agentId;
|
|
93
|
+
this.userId = config.userId ?? "anonymous";
|
|
94
|
+
this.keyPrefix = config.keyPrefix ?? DEFAULT_KEY_PREFIX;
|
|
95
|
+
this.ttl = config.ttl ?? DEFAULT_TTL;
|
|
96
|
+
this.config = config;
|
|
97
|
+
}
|
|
98
|
+
getKey() {
|
|
99
|
+
return `${this.keyPrefix}${this.agentId.length}:${this.agentId}:${this.userId.length}:${this.userId}`;
|
|
100
|
+
}
|
|
101
|
+
parseMessages(data) {
|
|
102
|
+
if (!data)
|
|
103
|
+
return [];
|
|
104
|
+
try {
|
|
105
|
+
return JSON.parse(data);
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
// Do NOT swallow-and-return []: add() would then overwrite the key with a
|
|
109
|
+
// single message and permanently destroy the stored history. Surface the
|
|
110
|
+
// corruption so the caller aborts instead of silently truncating.
|
|
111
|
+
agentLogger.error("Corrupted JSON in Redis memory; refusing to overwrite", {
|
|
112
|
+
errorName: error instanceof Error ? error.name : typeof error,
|
|
113
|
+
keyLength: this.getKey().length,
|
|
114
|
+
});
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
add(message) {
|
|
119
|
+
return withSpan("agent.memory.redis.add", async () => {
|
|
120
|
+
const key = this.getKey();
|
|
121
|
+
await this.atomicAdd(key, message);
|
|
122
|
+
}, { "memory.type": "redis", "memory.agent_id": this.agentId, "memory.ttl": this.ttl });
|
|
123
|
+
}
|
|
124
|
+
getMessages() {
|
|
125
|
+
return withSpan("agent.memory.redis.getMessages", async () => this.parseMessages(await this.client.get(this.getKey())), { "memory.type": "redis", "memory.agent_id": this.agentId });
|
|
126
|
+
}
|
|
127
|
+
clear() {
|
|
128
|
+
return withSpan("agent.memory.redis.clear", async () => {
|
|
129
|
+
await this.client.del(this.getKey());
|
|
130
|
+
}, { "memory.type": "redis", "memory.agent_id": this.agentId });
|
|
131
|
+
}
|
|
132
|
+
getStats() {
|
|
133
|
+
return withSpan("agent.memory.redis.getStats", async () => {
|
|
134
|
+
const messages = await this.getMessages();
|
|
135
|
+
return {
|
|
136
|
+
totalMessages: messages.length,
|
|
137
|
+
estimatedTokens: estimateTokens(messages),
|
|
138
|
+
type: "redis",
|
|
139
|
+
};
|
|
140
|
+
}, { "memory.type": "redis", "memory.agent_id": this.agentId });
|
|
141
|
+
}
|
|
142
|
+
touch() {
|
|
143
|
+
return withSpan("agent.memory.redis.touch", async () => {
|
|
144
|
+
// TTL <= 0 means no expiration, so touch is a no-op
|
|
145
|
+
if (this.ttl <= 0)
|
|
146
|
+
return;
|
|
147
|
+
await this.client.expire(this.getKey(), this.ttl);
|
|
148
|
+
}, { "memory.type": "redis", "memory.agent_id": this.agentId, "memory.ttl": this.ttl });
|
|
149
|
+
}
|
|
150
|
+
atomicAdd(key, message) {
|
|
151
|
+
const args = [
|
|
152
|
+
JSON.stringify(message),
|
|
153
|
+
String(this.config.maxMessages ?? 0),
|
|
154
|
+
String(this.config.maxTokens ?? 0),
|
|
155
|
+
String(this.ttl),
|
|
156
|
+
];
|
|
157
|
+
if (typeof this.client.sendCommand === "function") {
|
|
158
|
+
return this.client.sendCommand(["EVAL", ATOMIC_ADD_SCRIPT, "1", key, ...args]).then(() => { });
|
|
159
|
+
}
|
|
160
|
+
if (typeof this.client.call === "function") {
|
|
161
|
+
return this.client.call("EVAL", ATOMIC_ADD_SCRIPT, "1", key, ...args).then(() => { });
|
|
162
|
+
}
|
|
163
|
+
if (typeof this.client.eval === "function") {
|
|
164
|
+
return this.client.eval(ATOMIC_ADD_SCRIPT, { keys: [key], arguments: args }).then(() => { });
|
|
165
|
+
}
|
|
166
|
+
throw new Error("RedisMemory requires Redis sendCommand(), call(), or eval() for atomic add(); " +
|
|
167
|
+
"non-atomic GET/SET append is not supported");
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/** Create redis memory. */
|
|
171
|
+
export function createRedisMemory(agentId, config) {
|
|
172
|
+
return new RedisMemory(agentId, config);
|
|
173
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { DistributedCacheAdministration } from "veryfront/extensions/distributed/cache-support";
|
|
2
|
+
import type { RedisClientManager, RedisClientOptions } from "./redis-client-manager.js";
|
|
3
|
+
/** Keep Redis cursor and command semantics behind the provider contract. */
|
|
4
|
+
export declare function createRedisCacheAdministration(manager: RedisClientManager, clientOptions: RedisClientOptions): DistributedCacheAdministration;
|
|
5
|
+
//# sourceMappingURL=cache-administration.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-administration.d.ts","sourceRoot":"","sources":["../src/cache-administration.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,8BAA8B,EAG/B,MAAM,gDAAgD,CAAC;AACxD,OAAO,KAAK,EAEV,kBAAkB,EAClB,kBAAkB,EACnB,MAAM,2BAA2B,CAAC;AAiCnC,4EAA4E;AAC5E,wBAAgB,8BAA8B,CAC5C,OAAO,EAAE,kBAAkB,EAC3B,aAAa,EAAE,kBAAkB,GAChC,8BAA8B,CAmGhC"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { isRedisAtomicCounterKey, REDIS_LOGICAL_DELETE_SCRIPT } from "./cache-backend.js";
|
|
2
|
+
import { parseRevisionedCacheRecord } from "./revisioned-cache-record.js";
|
|
3
|
+
import { REVISIONED_CACHE_KEY_PREFIX, stripOwnedDistributedCacheKeyPrefix, } from "veryfront/extensions/distributed/cache-support";
|
|
4
|
+
const SCAN_COUNT = 100;
|
|
5
|
+
const DELETE_BATCH_SIZE = 1_000;
|
|
6
|
+
const MAX_SCAN_ITERATIONS = 10_000;
|
|
7
|
+
const MAX_SCANNED_KEYS = 100_000;
|
|
8
|
+
function escapeRedisGlobLiteral(value) {
|
|
9
|
+
return value.replace(/[\\*?\[\]]/g, "\\$&");
|
|
10
|
+
}
|
|
11
|
+
function requireScanResult(value, prefix) {
|
|
12
|
+
if (!value ||
|
|
13
|
+
!Number.isSafeInteger(value.cursor) ||
|
|
14
|
+
value.cursor < 0 ||
|
|
15
|
+
!Array.isArray(value.keys) ||
|
|
16
|
+
!value.keys.every((key) => typeof key === "string" && key.startsWith(prefix))) {
|
|
17
|
+
throw new TypeError("Redis returned an invalid cache-administration SCAN result");
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
/** Keep Redis cursor and command semantics behind the provider contract. */
|
|
22
|
+
export function createRedisCacheAdministration(manager, clientOptions) {
|
|
23
|
+
const connection = Object.freeze({ ...clientOptions });
|
|
24
|
+
return Object.freeze({
|
|
25
|
+
isConfigured() {
|
|
26
|
+
return manager.isConfigured(connection);
|
|
27
|
+
},
|
|
28
|
+
async listKeys({ prefix, limit }) {
|
|
29
|
+
const client = await manager.getClient(connection);
|
|
30
|
+
const keys = new Set();
|
|
31
|
+
const seenPhysicalKeys = new Set();
|
|
32
|
+
const seenCursors = new Set();
|
|
33
|
+
let cursor = 0;
|
|
34
|
+
let iterations = 0;
|
|
35
|
+
let scanned = 0;
|
|
36
|
+
do {
|
|
37
|
+
if (iterations++ >= MAX_SCAN_ITERATIONS) {
|
|
38
|
+
throw new RangeError("Redis cache administration exceeded the SCAN iteration limit");
|
|
39
|
+
}
|
|
40
|
+
if (seenCursors.has(cursor)) {
|
|
41
|
+
throw new Error("Redis cache administration repeated a SCAN cursor");
|
|
42
|
+
}
|
|
43
|
+
seenCursors.add(cursor);
|
|
44
|
+
const page = requireScanResult(await client.scan(cursor, {
|
|
45
|
+
MATCH: `${escapeRedisGlobLiteral(prefix)}*`,
|
|
46
|
+
COUNT: SCAN_COUNT,
|
|
47
|
+
}), prefix);
|
|
48
|
+
cursor = page.cursor;
|
|
49
|
+
scanned += page.keys.length;
|
|
50
|
+
if (scanned > MAX_SCANNED_KEYS) {
|
|
51
|
+
throw new RangeError("Redis cache administration exceeded the key traversal limit");
|
|
52
|
+
}
|
|
53
|
+
const candidates = page.keys.filter((key) => {
|
|
54
|
+
if (seenPhysicalKeys.has(key))
|
|
55
|
+
return false;
|
|
56
|
+
seenPhysicalKeys.add(key);
|
|
57
|
+
return true;
|
|
58
|
+
});
|
|
59
|
+
if (candidates.length === 0)
|
|
60
|
+
continue;
|
|
61
|
+
const values = await client.mGet([...candidates]);
|
|
62
|
+
if (!Array.isArray(values) ||
|
|
63
|
+
values.length !== candidates.length ||
|
|
64
|
+
!values.every((value) => value === null || typeof value === "string")) {
|
|
65
|
+
throw new TypeError("Redis returned an invalid cache-administration MGET result");
|
|
66
|
+
}
|
|
67
|
+
for (let index = 0; index < candidates.length; index++) {
|
|
68
|
+
const key = candidates[index];
|
|
69
|
+
const value = values[index];
|
|
70
|
+
if (value === null)
|
|
71
|
+
continue;
|
|
72
|
+
if (stripOwnedDistributedCacheKeyPrefix(key).startsWith(REVISIONED_CACHE_KEY_PREFIX)) {
|
|
73
|
+
const record = parseRevisionedCacheRecord(value);
|
|
74
|
+
if (record.kind === "absent")
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (keys.size === limit) {
|
|
78
|
+
return Object.freeze({ keys: Object.freeze([...keys]), truncated: true });
|
|
79
|
+
}
|
|
80
|
+
keys.add(key);
|
|
81
|
+
}
|
|
82
|
+
} while (cursor !== 0);
|
|
83
|
+
return Object.freeze({ keys: Object.freeze([...keys]), truncated: false });
|
|
84
|
+
},
|
|
85
|
+
async deleteKeys(keys) {
|
|
86
|
+
if (keys.length === 0)
|
|
87
|
+
return 0;
|
|
88
|
+
if (keys.some(isRedisAtomicCounterKey)) {
|
|
89
|
+
throw new TypeError("Redis atomic counters are protected from cache administration");
|
|
90
|
+
}
|
|
91
|
+
const client = await manager.getClient(connection);
|
|
92
|
+
let deleted = 0;
|
|
93
|
+
for (let index = 0; index < keys.length; index += DELETE_BATCH_SIZE) {
|
|
94
|
+
const batch = keys.slice(index, index + DELETE_BATCH_SIZE);
|
|
95
|
+
const classifications = batch.map((key) => stripOwnedDistributedCacheKeyPrefix(key).startsWith(REVISIONED_CACHE_KEY_PREFIX)
|
|
96
|
+
? "1"
|
|
97
|
+
: "0").join("");
|
|
98
|
+
const count = await client.eval(REDIS_LOGICAL_DELETE_SCRIPT, {
|
|
99
|
+
keys: batch,
|
|
100
|
+
arguments: [classifications, "vf-logical-delete-v1"],
|
|
101
|
+
});
|
|
102
|
+
if (typeof count !== "number" ||
|
|
103
|
+
!Number.isSafeInteger(count) ||
|
|
104
|
+
count < 0 ||
|
|
105
|
+
count > batch.length) {
|
|
106
|
+
throw new TypeError("Redis returned an invalid cache-administration DEL count");
|
|
107
|
+
}
|
|
108
|
+
deleted += count;
|
|
109
|
+
}
|
|
110
|
+
return deleted;
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { isRedisConfigured, type RedisClientManager, type RedisClientOptions } from "./redis-client-manager.js";
|
|
2
|
+
import { type CacheBackend, type RevisionedCacheBackend } from "veryfront/extensions/distributed/cache-support";
|
|
3
|
+
export declare const REDIS_LOGICAL_DELETE_SCRIPT: string;
|
|
4
|
+
/** Test whether a physical Redis key is the protected namespace counter. */
|
|
5
|
+
export declare function isRedisAtomicCounterKey(key: unknown): key is string;
|
|
6
|
+
export interface RedisCacheBackendOptions {
|
|
7
|
+
clientManager?: RedisClientManager;
|
|
8
|
+
clientOptions?: RedisClientOptions;
|
|
9
|
+
}
|
|
10
|
+
export { isRedisConfigured };
|
|
11
|
+
export declare class RedisCacheBackend implements CacheBackend {
|
|
12
|
+
readonly type: "distributed";
|
|
13
|
+
getWithRevision?: RevisionedCacheBackend["getWithRevision"];
|
|
14
|
+
compareExchange?: RevisionedCacheBackend["compareExchange"];
|
|
15
|
+
private readonly keyPrefix;
|
|
16
|
+
private readonly atomicCounterKey;
|
|
17
|
+
private readonly clientManager;
|
|
18
|
+
private readonly clientOptions;
|
|
19
|
+
constructor(keyPrefix?: string, options?: RedisCacheBackendOptions);
|
|
20
|
+
private prefixKey;
|
|
21
|
+
private resetAfterFailure;
|
|
22
|
+
private clearRevisionCapability;
|
|
23
|
+
private publishRevisionCapability;
|
|
24
|
+
private probeRevisionCapability;
|
|
25
|
+
private getClientForRead;
|
|
26
|
+
private requireClient;
|
|
27
|
+
initialize(): Promise<boolean>;
|
|
28
|
+
get(key: string): Promise<string | null>;
|
|
29
|
+
getWithinLimit(key: string, maximumBytes: number): Promise<string | null>;
|
|
30
|
+
private decodeOrdinaryRead;
|
|
31
|
+
getRemainingTtlSeconds(key: string): Promise<number | null>;
|
|
32
|
+
getBatch(keys: string[]): Promise<Map<string, string | null>>;
|
|
33
|
+
set(key: string, value: string, ttlSeconds?: any): Promise<void>;
|
|
34
|
+
setBatch(entries: Array<{
|
|
35
|
+
key: string;
|
|
36
|
+
value: string;
|
|
37
|
+
ttl?: number;
|
|
38
|
+
}>): Promise<void>;
|
|
39
|
+
del(key: string): Promise<void>;
|
|
40
|
+
delByPattern(pattern: string): Promise<number>;
|
|
41
|
+
private assertDeleteCount;
|
|
42
|
+
private requireRevisionedKey;
|
|
43
|
+
private readWithRevision;
|
|
44
|
+
private exchangeRevision;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=cache-backend.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-backend.d.ts","sourceRoot":"","sources":["../src/cache-backend.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,iBAAiB,EAEjB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACxB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAKL,KAAK,YAAY,EAUjB,KAAK,sBAAsB,EAE5B,MAAM,gDAAgD,CAAC;AAqNxD,eAAO,MAAM,2BAA2B,QAyCvC,CAAC;AAyIF,4EAA4E;AAC5E,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,MAAM,CAEnE;AAQD,MAAM,WAAW,wBAAwB;IACvC,aAAa,CAAC,EAAE,kBAAkB,CAAC;IACnC,aAAa,CAAC,EAAE,kBAAkB,CAAC;CACpC;AAGD,OAAO,EAAE,iBAAiB,EAAE,CAAC;AAE7B,qBAAa,iBAAkB,YAAW,YAAY;IACpD,QAAQ,CAAC,IAAI,EAAG,aAAa,CAAU;IAC/B,eAAe,CAAC,EAAE,sBAAsB,CAAC,iBAAiB,CAAC,CAAC;IAC5D,eAAe,CAAC,EAAE,sBAAsB,CAAC,iBAAiB,CAAC,CAAC;IACpE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAC1C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAqB;IACnD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAqB;gBAEvC,SAAS,SAAsB,EAAE,OAAO,GAAE,wBAA6B;IAOnF,OAAO,CAAC,SAAS;YAIH,iBAAiB;IAS/B,OAAO,CAAC,uBAAuB;IAK/B,OAAO,CAAC,yBAAyB;YAiBnB,uBAAuB;YAyBvB,gBAAgB;YAYhB,aAAa;IAO3B,UAAU,IAAI,OAAO,CAAC,OAAO,CAAC;IA0BxB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAiBxC,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAkC/E,OAAO,CAAC,kBAAkB;IASpB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAiB3D,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC;IAsC7D,GAAG,CACP,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EACb,UAAU,MAA4B,GACrC,OAAO,CAAC,IAAI,CAAC;IA0BV,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IA8BrF,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAe/B,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAiEpD,OAAO,CAAC,iBAAiB;IAWzB,OAAO,CAAC,oBAAoB;YAOd,gBAAgB;YAehB,gBAAgB;CA4B/B"}
|