@dudousxd/nestjs-agent 0.3.2 → 0.3.3
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-deps.factory-DpVkZYCy.d.cts → agent-deps.factory-D4KyREOv.d.cts} +11 -0
- package/dist/{agent-deps.factory-DpVkZYCy.d.ts → agent-deps.factory-D4KyREOv.d.ts} +11 -0
- package/dist/durable/index.cjs +38 -9
- package/dist/durable/index.cjs.map +1 -1
- package/dist/durable/index.d.cts +4 -4
- package/dist/durable/index.d.ts +4 -4
- package/dist/durable/index.js +38 -9
- package/dist/durable/index.js.map +1 -1
- package/dist/index.cjs +27 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -3
- package/dist/index.d.ts +5 -3
- package/dist/index.js +27 -7
- package/dist/index.js.map +1 -1
- package/dist/sink-redis/index.cjs +144 -0
- package/dist/sink-redis/index.cjs.map +1 -0
- package/dist/sink-redis/index.d.cts +39 -0
- package/dist/sink-redis/index.d.ts +39 -0
- package/dist/sink-redis/index.js +119 -0
- package/dist/sink-redis/index.js.map +1 -0
- package/package.json +20 -5
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/sink-redis/index.ts
|
|
22
|
+
var sink_redis_exports = {};
|
|
23
|
+
__export(sink_redis_exports, {
|
|
24
|
+
RedisTokenStreamSink: () => RedisTokenStreamSink
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(sink_redis_exports);
|
|
27
|
+
|
|
28
|
+
// src/sink-redis/redis-token-stream-sink.ts
|
|
29
|
+
var import_nestjs_agent_core = require("@dudousxd/nestjs-agent-core");
|
|
30
|
+
var CHUNK_FIELD = "c";
|
|
31
|
+
var END_FIELD = "e";
|
|
32
|
+
var FAIL_FIELD = "f";
|
|
33
|
+
var RedisTokenStreamSink = class {
|
|
34
|
+
static {
|
|
35
|
+
__name(this, "RedisTokenStreamSink");
|
|
36
|
+
}
|
|
37
|
+
redis;
|
|
38
|
+
keyPrefix;
|
|
39
|
+
ttlSeconds;
|
|
40
|
+
blockMs;
|
|
41
|
+
constructor(redis, options = {}) {
|
|
42
|
+
this.redis = redis;
|
|
43
|
+
this.keyPrefix = options.keyPrefix ?? "agent:sink:";
|
|
44
|
+
this.ttlSeconds = options.ttlSeconds ?? 3600;
|
|
45
|
+
this.blockMs = options.blockMs ?? 5e3;
|
|
46
|
+
}
|
|
47
|
+
key(runId) {
|
|
48
|
+
return `${this.keyPrefix}${runId}`;
|
|
49
|
+
}
|
|
50
|
+
open(runId) {
|
|
51
|
+
const key = this.key(runId);
|
|
52
|
+
return {
|
|
53
|
+
write: /* @__PURE__ */ __name(async (chunk) => {
|
|
54
|
+
await this.redis.xadd(key, "*", CHUNK_FIELD, Buffer.from(chunk));
|
|
55
|
+
}, "write"),
|
|
56
|
+
end: /* @__PURE__ */ __name(async () => {
|
|
57
|
+
await this.redis.xadd(key, "*", END_FIELD, "1");
|
|
58
|
+
await this.redis.expire(key, this.ttlSeconds);
|
|
59
|
+
}, "end"),
|
|
60
|
+
fail: /* @__PURE__ */ __name(async (error) => {
|
|
61
|
+
await this.redis.xadd(key, "*", FAIL_FIELD, JSON.stringify(error));
|
|
62
|
+
await this.redis.expire(key, this.ttlSeconds);
|
|
63
|
+
}, "fail")
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
async *subscribe(runId) {
|
|
67
|
+
const key = this.key(runId);
|
|
68
|
+
const reader = this.redis.duplicate();
|
|
69
|
+
try {
|
|
70
|
+
let lastId = "0";
|
|
71
|
+
while (true) {
|
|
72
|
+
const response = await reader.xreadBuffer("BLOCK", this.blockMs, "STREAMS", key, lastId);
|
|
73
|
+
for (const entry of readEntries(response)) {
|
|
74
|
+
lastId = entry.id;
|
|
75
|
+
if (entry.field === CHUNK_FIELD && entry.value !== void 0) {
|
|
76
|
+
yield new Uint8Array(entry.value);
|
|
77
|
+
} else if (entry.field === END_FIELD) {
|
|
78
|
+
return;
|
|
79
|
+
} else if (entry.field === FAIL_FIELD && entry.value !== void 0) {
|
|
80
|
+
throw new import_nestjs_agent_core.AgentStreamError(parseStreamError(entry.value.toString()));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} finally {
|
|
85
|
+
reader.disconnect();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async close(runId) {
|
|
89
|
+
await this.redis.del(this.key(runId));
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
function readEntries(response) {
|
|
93
|
+
if (!Array.isArray(response)) {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
const entries = [];
|
|
97
|
+
for (const stream of response) {
|
|
98
|
+
if (!Array.isArray(stream) || !Array.isArray(stream[1])) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
for (const item of stream[1]) {
|
|
102
|
+
if (!Array.isArray(item) || item.length < 2) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const [id, fields] = item;
|
|
106
|
+
if (!Buffer.isBuffer(id) || !Array.isArray(fields)) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const field = Buffer.isBuffer(fields[0]) ? fields[0].toString() : void 0;
|
|
110
|
+
if (field === void 0) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
entries.push({
|
|
114
|
+
id: id.toString(),
|
|
115
|
+
field,
|
|
116
|
+
value: Buffer.isBuffer(fields[1]) ? fields[1] : void 0
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return entries;
|
|
121
|
+
}
|
|
122
|
+
__name(readEntries, "readEntries");
|
|
123
|
+
function parseStreamError(raw) {
|
|
124
|
+
try {
|
|
125
|
+
const parsed = JSON.parse(raw);
|
|
126
|
+
if (typeof parsed === "object" && parsed !== null && "code" in parsed && "message" in parsed && typeof parsed.code === "string" && typeof parsed.message === "string") {
|
|
127
|
+
return {
|
|
128
|
+
code: parsed.code,
|
|
129
|
+
message: parsed.message
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
} catch {
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
code: "stream_failed",
|
|
136
|
+
message: raw
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
__name(parseStreamError, "parseStreamError");
|
|
140
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
141
|
+
0 && (module.exports = {
|
|
142
|
+
RedisTokenStreamSink
|
|
143
|
+
});
|
|
144
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/sink-redis/index.ts","../../src/sink-redis/redis-token-stream-sink.ts"],"sourcesContent":["export {\n RedisTokenStreamSink,\n type RedisTokenStreamSinkOptions,\n} from './redis-token-stream-sink.js';\n","import {\n AgentStreamError,\n type SinkWriter,\n type StreamError,\n type TokenStreamSink,\n} from '@dudousxd/nestjs-agent-core';\nimport type { Redis } from 'ioredis';\n\nexport interface RedisTokenStreamSinkOptions {\n /** Key namespace for the per-run streams. Default `agent:sink:`. */\n keyPrefix?: string;\n /**\n * TTL (seconds) applied to a run's stream once it ends/fails, so completed streams self-expire\n * instead of accumulating in Redis. A subscriber that reconnects within the window still replays.\n * Default 3600 (1h).\n */\n ttlSeconds?: number;\n /** `XREAD BLOCK` timeout (ms) between follow polls. Default 5000. Purely an internal cadence. */\n blockMs?: number;\n}\n\n// One-character field names keep each stream entry's payload minimal (one XADD per token delta).\nconst CHUNK_FIELD = 'c';\nconst END_FIELD = 'e';\nconst FAIL_FIELD = 'f';\n\n/**\n * A cross-process {@link TokenStreamSink} backed by Redis Streams — the \"data plane\" for durable\n * agent turns whose model call runs on a WORKER pod while the SSE connection is served by an API\n * pod. The turn `XADD`s each delta to `agent:sink:<runId>`; a subscriber `XREAD`s from `0` so it\n * REPLAYS everything buffered so far (a reconnect or a subscriber that attaches after the run\n * started misses nothing), then blocks for live entries until an `end`/`fail` sentinel.\n *\n * The consumer supplies the ioredis client (so the lib stays Redis-driver-agnostic and reuses the\n * app's connection config). Blocking reads monopolise a connection, so each `subscribe` runs on a\n * short-lived `duplicate()` that is disconnected when the subscriber stops.\n */\nexport class RedisTokenStreamSink implements TokenStreamSink {\n private readonly keyPrefix: string;\n private readonly ttlSeconds: number;\n private readonly blockMs: number;\n\n constructor(\n private readonly redis: Redis,\n options: RedisTokenStreamSinkOptions = {},\n ) {\n this.keyPrefix = options.keyPrefix ?? 'agent:sink:';\n this.ttlSeconds = options.ttlSeconds ?? 3600;\n this.blockMs = options.blockMs ?? 5000;\n }\n\n private key(runId: string): string {\n return `${this.keyPrefix}${runId}`;\n }\n\n open(runId: string): SinkWriter {\n const key = this.key(runId);\n return {\n write: async (chunk: Uint8Array) => {\n await this.redis.xadd(key, '*', CHUNK_FIELD, Buffer.from(chunk));\n },\n end: async () => {\n await this.redis.xadd(key, '*', END_FIELD, '1');\n await this.redis.expire(key, this.ttlSeconds);\n },\n fail: async (error: StreamError) => {\n await this.redis.xadd(key, '*', FAIL_FIELD, JSON.stringify(error));\n await this.redis.expire(key, this.ttlSeconds);\n },\n };\n }\n\n async *subscribe(runId: string): AsyncIterable<Uint8Array> {\n const key = this.key(runId);\n // Blocking XREAD holds the connection for the whole poll, so follow on a dedicated one.\n const reader = this.redis.duplicate();\n try {\n let lastId = '0';\n while (true) {\n const response = await reader.xreadBuffer('BLOCK', this.blockMs, 'STREAMS', key, lastId);\n for (const entry of readEntries(response)) {\n lastId = entry.id;\n if (entry.field === CHUNK_FIELD && entry.value !== undefined) {\n yield new Uint8Array(entry.value);\n } else if (entry.field === END_FIELD) {\n return;\n } else if (entry.field === FAIL_FIELD && entry.value !== undefined) {\n throw new AgentStreamError(parseStreamError(entry.value.toString()));\n }\n }\n }\n } finally {\n reader.disconnect();\n }\n }\n\n async close(runId: string): Promise<void> {\n await this.redis.del(this.key(runId));\n }\n}\n\ninterface StreamEntry {\n id: string;\n field: string;\n value: Buffer | undefined;\n}\n\n/**\n * Flatten ioredis's `XREAD` buffer response — `[[key, [[id, [field, value, …]], …]], …] | null` —\n * into the entries we care about. Validated defensively at runtime (it crosses the Redis boundary)\n * rather than trusting the driver's loose typing.\n */\nfunction readEntries(response: unknown): StreamEntry[] {\n if (!Array.isArray(response)) {\n return [];\n }\n const entries: StreamEntry[] = [];\n for (const stream of response) {\n if (!Array.isArray(stream) || !Array.isArray(stream[1])) {\n continue;\n }\n for (const item of stream[1]) {\n if (!Array.isArray(item) || item.length < 2) {\n continue;\n }\n const [id, fields] = item;\n if (!Buffer.isBuffer(id) || !Array.isArray(fields)) {\n continue;\n }\n const field = Buffer.isBuffer(fields[0]) ? fields[0].toString() : undefined;\n if (field === undefined) {\n continue;\n }\n entries.push({\n id: id.toString(),\n field,\n value: Buffer.isBuffer(fields[1]) ? fields[1] : undefined,\n });\n }\n }\n return entries;\n}\n\n/** Parse a `fail` sentinel's payload back into a {@link StreamError}, tolerating malformed JSON. */\nfunction parseStreamError(raw: string): StreamError {\n try {\n const parsed: unknown = JSON.parse(raw);\n if (\n typeof parsed === 'object' &&\n parsed !== null &&\n 'code' in parsed &&\n 'message' in parsed &&\n typeof parsed.code === 'string' &&\n typeof parsed.message === 'string'\n ) {\n return { code: parsed.code, message: parsed.message };\n }\n } catch {\n // fall through to the generic terminal below\n }\n return { code: 'stream_failed', message: raw };\n}\n"],"mappings":"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","names":["CHUNK_FIELD","END_FIELD","FAIL_FIELD","RedisTokenStreamSink","keyPrefix","ttlSeconds","blockMs","redis","options","key","runId","open","write","chunk","xadd","Buffer","from","end","expire","fail","error","JSON","stringify","subscribe","reader","duplicate","lastId","response","xreadBuffer","entry","readEntries","id","field","value","undefined","Uint8Array","AgentStreamError","parseStreamError","toString","disconnect","close","del","Array","isArray","entries","stream","item","length","fields","isBuffer","push","raw","parsed","parse","code","message"]}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { TokenStreamSink, SinkWriter } from '@dudousxd/nestjs-agent-core';
|
|
2
|
+
import { Redis } from 'ioredis';
|
|
3
|
+
|
|
4
|
+
interface RedisTokenStreamSinkOptions {
|
|
5
|
+
/** Key namespace for the per-run streams. Default `agent:sink:`. */
|
|
6
|
+
keyPrefix?: string;
|
|
7
|
+
/**
|
|
8
|
+
* TTL (seconds) applied to a run's stream once it ends/fails, so completed streams self-expire
|
|
9
|
+
* instead of accumulating in Redis. A subscriber that reconnects within the window still replays.
|
|
10
|
+
* Default 3600 (1h).
|
|
11
|
+
*/
|
|
12
|
+
ttlSeconds?: number;
|
|
13
|
+
/** `XREAD BLOCK` timeout (ms) between follow polls. Default 5000. Purely an internal cadence. */
|
|
14
|
+
blockMs?: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A cross-process {@link TokenStreamSink} backed by Redis Streams — the "data plane" for durable
|
|
18
|
+
* agent turns whose model call runs on a WORKER pod while the SSE connection is served by an API
|
|
19
|
+
* pod. The turn `XADD`s each delta to `agent:sink:<runId>`; a subscriber `XREAD`s from `0` so it
|
|
20
|
+
* REPLAYS everything buffered so far (a reconnect or a subscriber that attaches after the run
|
|
21
|
+
* started misses nothing), then blocks for live entries until an `end`/`fail` sentinel.
|
|
22
|
+
*
|
|
23
|
+
* The consumer supplies the ioredis client (so the lib stays Redis-driver-agnostic and reuses the
|
|
24
|
+
* app's connection config). Blocking reads monopolise a connection, so each `subscribe` runs on a
|
|
25
|
+
* short-lived `duplicate()` that is disconnected when the subscriber stops.
|
|
26
|
+
*/
|
|
27
|
+
declare class RedisTokenStreamSink implements TokenStreamSink {
|
|
28
|
+
private readonly redis;
|
|
29
|
+
private readonly keyPrefix;
|
|
30
|
+
private readonly ttlSeconds;
|
|
31
|
+
private readonly blockMs;
|
|
32
|
+
constructor(redis: Redis, options?: RedisTokenStreamSinkOptions);
|
|
33
|
+
private key;
|
|
34
|
+
open(runId: string): SinkWriter;
|
|
35
|
+
subscribe(runId: string): AsyncIterable<Uint8Array>;
|
|
36
|
+
close(runId: string): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export { RedisTokenStreamSink, type RedisTokenStreamSinkOptions };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { TokenStreamSink, SinkWriter } from '@dudousxd/nestjs-agent-core';
|
|
2
|
+
import { Redis } from 'ioredis';
|
|
3
|
+
|
|
4
|
+
interface RedisTokenStreamSinkOptions {
|
|
5
|
+
/** Key namespace for the per-run streams. Default `agent:sink:`. */
|
|
6
|
+
keyPrefix?: string;
|
|
7
|
+
/**
|
|
8
|
+
* TTL (seconds) applied to a run's stream once it ends/fails, so completed streams self-expire
|
|
9
|
+
* instead of accumulating in Redis. A subscriber that reconnects within the window still replays.
|
|
10
|
+
* Default 3600 (1h).
|
|
11
|
+
*/
|
|
12
|
+
ttlSeconds?: number;
|
|
13
|
+
/** `XREAD BLOCK` timeout (ms) between follow polls. Default 5000. Purely an internal cadence. */
|
|
14
|
+
blockMs?: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A cross-process {@link TokenStreamSink} backed by Redis Streams — the "data plane" for durable
|
|
18
|
+
* agent turns whose model call runs on a WORKER pod while the SSE connection is served by an API
|
|
19
|
+
* pod. The turn `XADD`s each delta to `agent:sink:<runId>`; a subscriber `XREAD`s from `0` so it
|
|
20
|
+
* REPLAYS everything buffered so far (a reconnect or a subscriber that attaches after the run
|
|
21
|
+
* started misses nothing), then blocks for live entries until an `end`/`fail` sentinel.
|
|
22
|
+
*
|
|
23
|
+
* The consumer supplies the ioredis client (so the lib stays Redis-driver-agnostic and reuses the
|
|
24
|
+
* app's connection config). Blocking reads monopolise a connection, so each `subscribe` runs on a
|
|
25
|
+
* short-lived `duplicate()` that is disconnected when the subscriber stops.
|
|
26
|
+
*/
|
|
27
|
+
declare class RedisTokenStreamSink implements TokenStreamSink {
|
|
28
|
+
private readonly redis;
|
|
29
|
+
private readonly keyPrefix;
|
|
30
|
+
private readonly ttlSeconds;
|
|
31
|
+
private readonly blockMs;
|
|
32
|
+
constructor(redis: Redis, options?: RedisTokenStreamSinkOptions);
|
|
33
|
+
private key;
|
|
34
|
+
open(runId: string): SinkWriter;
|
|
35
|
+
subscribe(runId: string): AsyncIterable<Uint8Array>;
|
|
36
|
+
close(runId: string): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export { RedisTokenStreamSink, type RedisTokenStreamSinkOptions };
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
+
|
|
4
|
+
// src/sink-redis/redis-token-stream-sink.ts
|
|
5
|
+
import { AgentStreamError } from "@dudousxd/nestjs-agent-core";
|
|
6
|
+
var CHUNK_FIELD = "c";
|
|
7
|
+
var END_FIELD = "e";
|
|
8
|
+
var FAIL_FIELD = "f";
|
|
9
|
+
var RedisTokenStreamSink = class {
|
|
10
|
+
static {
|
|
11
|
+
__name(this, "RedisTokenStreamSink");
|
|
12
|
+
}
|
|
13
|
+
redis;
|
|
14
|
+
keyPrefix;
|
|
15
|
+
ttlSeconds;
|
|
16
|
+
blockMs;
|
|
17
|
+
constructor(redis, options = {}) {
|
|
18
|
+
this.redis = redis;
|
|
19
|
+
this.keyPrefix = options.keyPrefix ?? "agent:sink:";
|
|
20
|
+
this.ttlSeconds = options.ttlSeconds ?? 3600;
|
|
21
|
+
this.blockMs = options.blockMs ?? 5e3;
|
|
22
|
+
}
|
|
23
|
+
key(runId) {
|
|
24
|
+
return `${this.keyPrefix}${runId}`;
|
|
25
|
+
}
|
|
26
|
+
open(runId) {
|
|
27
|
+
const key = this.key(runId);
|
|
28
|
+
return {
|
|
29
|
+
write: /* @__PURE__ */ __name(async (chunk) => {
|
|
30
|
+
await this.redis.xadd(key, "*", CHUNK_FIELD, Buffer.from(chunk));
|
|
31
|
+
}, "write"),
|
|
32
|
+
end: /* @__PURE__ */ __name(async () => {
|
|
33
|
+
await this.redis.xadd(key, "*", END_FIELD, "1");
|
|
34
|
+
await this.redis.expire(key, this.ttlSeconds);
|
|
35
|
+
}, "end"),
|
|
36
|
+
fail: /* @__PURE__ */ __name(async (error) => {
|
|
37
|
+
await this.redis.xadd(key, "*", FAIL_FIELD, JSON.stringify(error));
|
|
38
|
+
await this.redis.expire(key, this.ttlSeconds);
|
|
39
|
+
}, "fail")
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
async *subscribe(runId) {
|
|
43
|
+
const key = this.key(runId);
|
|
44
|
+
const reader = this.redis.duplicate();
|
|
45
|
+
try {
|
|
46
|
+
let lastId = "0";
|
|
47
|
+
while (true) {
|
|
48
|
+
const response = await reader.xreadBuffer("BLOCK", this.blockMs, "STREAMS", key, lastId);
|
|
49
|
+
for (const entry of readEntries(response)) {
|
|
50
|
+
lastId = entry.id;
|
|
51
|
+
if (entry.field === CHUNK_FIELD && entry.value !== void 0) {
|
|
52
|
+
yield new Uint8Array(entry.value);
|
|
53
|
+
} else if (entry.field === END_FIELD) {
|
|
54
|
+
return;
|
|
55
|
+
} else if (entry.field === FAIL_FIELD && entry.value !== void 0) {
|
|
56
|
+
throw new AgentStreamError(parseStreamError(entry.value.toString()));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} finally {
|
|
61
|
+
reader.disconnect();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async close(runId) {
|
|
65
|
+
await this.redis.del(this.key(runId));
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
function readEntries(response) {
|
|
69
|
+
if (!Array.isArray(response)) {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
const entries = [];
|
|
73
|
+
for (const stream of response) {
|
|
74
|
+
if (!Array.isArray(stream) || !Array.isArray(stream[1])) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
for (const item of stream[1]) {
|
|
78
|
+
if (!Array.isArray(item) || item.length < 2) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const [id, fields] = item;
|
|
82
|
+
if (!Buffer.isBuffer(id) || !Array.isArray(fields)) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const field = Buffer.isBuffer(fields[0]) ? fields[0].toString() : void 0;
|
|
86
|
+
if (field === void 0) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
entries.push({
|
|
90
|
+
id: id.toString(),
|
|
91
|
+
field,
|
|
92
|
+
value: Buffer.isBuffer(fields[1]) ? fields[1] : void 0
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return entries;
|
|
97
|
+
}
|
|
98
|
+
__name(readEntries, "readEntries");
|
|
99
|
+
function parseStreamError(raw) {
|
|
100
|
+
try {
|
|
101
|
+
const parsed = JSON.parse(raw);
|
|
102
|
+
if (typeof parsed === "object" && parsed !== null && "code" in parsed && "message" in parsed && typeof parsed.code === "string" && typeof parsed.message === "string") {
|
|
103
|
+
return {
|
|
104
|
+
code: parsed.code,
|
|
105
|
+
message: parsed.message
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
code: "stream_failed",
|
|
112
|
+
message: raw
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
__name(parseStreamError, "parseStreamError");
|
|
116
|
+
export {
|
|
117
|
+
RedisTokenStreamSink
|
|
118
|
+
};
|
|
119
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/sink-redis/redis-token-stream-sink.ts"],"sourcesContent":["import {\n AgentStreamError,\n type SinkWriter,\n type StreamError,\n type TokenStreamSink,\n} from '@dudousxd/nestjs-agent-core';\nimport type { Redis } from 'ioredis';\n\nexport interface RedisTokenStreamSinkOptions {\n /** Key namespace for the per-run streams. Default `agent:sink:`. */\n keyPrefix?: string;\n /**\n * TTL (seconds) applied to a run's stream once it ends/fails, so completed streams self-expire\n * instead of accumulating in Redis. A subscriber that reconnects within the window still replays.\n * Default 3600 (1h).\n */\n ttlSeconds?: number;\n /** `XREAD BLOCK` timeout (ms) between follow polls. Default 5000. Purely an internal cadence. */\n blockMs?: number;\n}\n\n// One-character field names keep each stream entry's payload minimal (one XADD per token delta).\nconst CHUNK_FIELD = 'c';\nconst END_FIELD = 'e';\nconst FAIL_FIELD = 'f';\n\n/**\n * A cross-process {@link TokenStreamSink} backed by Redis Streams — the \"data plane\" for durable\n * agent turns whose model call runs on a WORKER pod while the SSE connection is served by an API\n * pod. The turn `XADD`s each delta to `agent:sink:<runId>`; a subscriber `XREAD`s from `0` so it\n * REPLAYS everything buffered so far (a reconnect or a subscriber that attaches after the run\n * started misses nothing), then blocks for live entries until an `end`/`fail` sentinel.\n *\n * The consumer supplies the ioredis client (so the lib stays Redis-driver-agnostic and reuses the\n * app's connection config). Blocking reads monopolise a connection, so each `subscribe` runs on a\n * short-lived `duplicate()` that is disconnected when the subscriber stops.\n */\nexport class RedisTokenStreamSink implements TokenStreamSink {\n private readonly keyPrefix: string;\n private readonly ttlSeconds: number;\n private readonly blockMs: number;\n\n constructor(\n private readonly redis: Redis,\n options: RedisTokenStreamSinkOptions = {},\n ) {\n this.keyPrefix = options.keyPrefix ?? 'agent:sink:';\n this.ttlSeconds = options.ttlSeconds ?? 3600;\n this.blockMs = options.blockMs ?? 5000;\n }\n\n private key(runId: string): string {\n return `${this.keyPrefix}${runId}`;\n }\n\n open(runId: string): SinkWriter {\n const key = this.key(runId);\n return {\n write: async (chunk: Uint8Array) => {\n await this.redis.xadd(key, '*', CHUNK_FIELD, Buffer.from(chunk));\n },\n end: async () => {\n await this.redis.xadd(key, '*', END_FIELD, '1');\n await this.redis.expire(key, this.ttlSeconds);\n },\n fail: async (error: StreamError) => {\n await this.redis.xadd(key, '*', FAIL_FIELD, JSON.stringify(error));\n await this.redis.expire(key, this.ttlSeconds);\n },\n };\n }\n\n async *subscribe(runId: string): AsyncIterable<Uint8Array> {\n const key = this.key(runId);\n // Blocking XREAD holds the connection for the whole poll, so follow on a dedicated one.\n const reader = this.redis.duplicate();\n try {\n let lastId = '0';\n while (true) {\n const response = await reader.xreadBuffer('BLOCK', this.blockMs, 'STREAMS', key, lastId);\n for (const entry of readEntries(response)) {\n lastId = entry.id;\n if (entry.field === CHUNK_FIELD && entry.value !== undefined) {\n yield new Uint8Array(entry.value);\n } else if (entry.field === END_FIELD) {\n return;\n } else if (entry.field === FAIL_FIELD && entry.value !== undefined) {\n throw new AgentStreamError(parseStreamError(entry.value.toString()));\n }\n }\n }\n } finally {\n reader.disconnect();\n }\n }\n\n async close(runId: string): Promise<void> {\n await this.redis.del(this.key(runId));\n }\n}\n\ninterface StreamEntry {\n id: string;\n field: string;\n value: Buffer | undefined;\n}\n\n/**\n * Flatten ioredis's `XREAD` buffer response — `[[key, [[id, [field, value, …]], …]], …] | null` —\n * into the entries we care about. Validated defensively at runtime (it crosses the Redis boundary)\n * rather than trusting the driver's loose typing.\n */\nfunction readEntries(response: unknown): StreamEntry[] {\n if (!Array.isArray(response)) {\n return [];\n }\n const entries: StreamEntry[] = [];\n for (const stream of response) {\n if (!Array.isArray(stream) || !Array.isArray(stream[1])) {\n continue;\n }\n for (const item of stream[1]) {\n if (!Array.isArray(item) || item.length < 2) {\n continue;\n }\n const [id, fields] = item;\n if (!Buffer.isBuffer(id) || !Array.isArray(fields)) {\n continue;\n }\n const field = Buffer.isBuffer(fields[0]) ? fields[0].toString() : undefined;\n if (field === undefined) {\n continue;\n }\n entries.push({\n id: id.toString(),\n field,\n value: Buffer.isBuffer(fields[1]) ? fields[1] : undefined,\n });\n }\n }\n return entries;\n}\n\n/** Parse a `fail` sentinel's payload back into a {@link StreamError}, tolerating malformed JSON. */\nfunction parseStreamError(raw: string): StreamError {\n try {\n const parsed: unknown = JSON.parse(raw);\n if (\n typeof parsed === 'object' &&\n parsed !== null &&\n 'code' in parsed &&\n 'message' in parsed &&\n typeof parsed.code === 'string' &&\n typeof parsed.message === 'string'\n ) {\n return { code: parsed.code, message: parsed.message };\n }\n } catch {\n // fall through to the generic terminal below\n }\n return { code: 'stream_failed', message: raw };\n}\n"],"mappings":";;;;AAAA,SACEA,wBAIK;AAiBP,IAAMC,cAAc;AACpB,IAAMC,YAAY;AAClB,IAAMC,aAAa;AAaZ,IAAMC,uBAAN,MAAMA;EArCb,OAqCaA;;;;EACMC;EACAC;EACAC;EAEjB,YACmBC,OACjBC,UAAuC,CAAC,GACxC;SAFiBD,QAAAA;AAGjB,SAAKH,YAAYI,QAAQJ,aAAa;AACtC,SAAKC,aAAaG,QAAQH,cAAc;AACxC,SAAKC,UAAUE,QAAQF,WAAW;EACpC;EAEQG,IAAIC,OAAuB;AACjC,WAAO,GAAG,KAAKN,SAAS,GAAGM,KAAAA;EAC7B;EAEAC,KAAKD,OAA2B;AAC9B,UAAMD,MAAM,KAAKA,IAAIC,KAAAA;AACrB,WAAO;MACLE,OAAO,8BAAOC,UAAAA;AACZ,cAAM,KAAKN,MAAMO,KAAKL,KAAK,KAAKT,aAAae,OAAOC,KAAKH,KAAAA,CAAAA;MAC3D,GAFO;MAGPI,KAAK,mCAAA;AACH,cAAM,KAAKV,MAAMO,KAAKL,KAAK,KAAKR,WAAW,GAAA;AAC3C,cAAM,KAAKM,MAAMW,OAAOT,KAAK,KAAKJ,UAAU;MAC9C,GAHK;MAILc,MAAM,8BAAOC,UAAAA;AACX,cAAM,KAAKb,MAAMO,KAAKL,KAAK,KAAKP,YAAYmB,KAAKC,UAAUF,KAAAA,CAAAA;AAC3D,cAAM,KAAKb,MAAMW,OAAOT,KAAK,KAAKJ,UAAU;MAC9C,GAHM;IAIR;EACF;EAEA,OAAOkB,UAAUb,OAA0C;AACzD,UAAMD,MAAM,KAAKA,IAAIC,KAAAA;AAErB,UAAMc,SAAS,KAAKjB,MAAMkB,UAAS;AACnC,QAAI;AACF,UAAIC,SAAS;AACb,aAAO,MAAM;AACX,cAAMC,WAAW,MAAMH,OAAOI,YAAY,SAAS,KAAKtB,SAAS,WAAWG,KAAKiB,MAAAA;AACjF,mBAAWG,SAASC,YAAYH,QAAAA,GAAW;AACzCD,mBAASG,MAAME;AACf,cAAIF,MAAMG,UAAUhC,eAAe6B,MAAMI,UAAUC,QAAW;AAC5D,kBAAM,IAAIC,WAAWN,MAAMI,KAAK;UAClC,WAAWJ,MAAMG,UAAU/B,WAAW;AACpC;UACF,WAAW4B,MAAMG,UAAU9B,cAAc2B,MAAMI,UAAUC,QAAW;AAClE,kBAAM,IAAIE,iBAAiBC,iBAAiBR,MAAMI,MAAMK,SAAQ,CAAA,CAAA;UAClE;QACF;MACF;IACF,UAAA;AACEd,aAAOe,WAAU;IACnB;EACF;EAEA,MAAMC,MAAM9B,OAA8B;AACxC,UAAM,KAAKH,MAAMkC,IAAI,KAAKhC,IAAIC,KAAAA,CAAAA;EAChC;AACF;AAaA,SAASoB,YAAYH,UAAiB;AACpC,MAAI,CAACe,MAAMC,QAAQhB,QAAAA,GAAW;AAC5B,WAAO,CAAA;EACT;AACA,QAAMiB,UAAyB,CAAA;AAC/B,aAAWC,UAAUlB,UAAU;AAC7B,QAAI,CAACe,MAAMC,QAAQE,MAAAA,KAAW,CAACH,MAAMC,QAAQE,OAAO,CAAA,CAAE,GAAG;AACvD;IACF;AACA,eAAWC,QAAQD,OAAO,CAAA,GAAI;AAC5B,UAAI,CAACH,MAAMC,QAAQG,IAAAA,KAASA,KAAKC,SAAS,GAAG;AAC3C;MACF;AACA,YAAM,CAAChB,IAAIiB,MAAAA,IAAUF;AACrB,UAAI,CAAC/B,OAAOkC,SAASlB,EAAAA,KAAO,CAACW,MAAMC,QAAQK,MAAAA,GAAS;AAClD;MACF;AACA,YAAMhB,QAAQjB,OAAOkC,SAASD,OAAO,CAAA,CAAE,IAAIA,OAAO,CAAA,EAAGV,SAAQ,IAAKJ;AAClE,UAAIF,UAAUE,QAAW;AACvB;MACF;AACAU,cAAQM,KAAK;QACXnB,IAAIA,GAAGO,SAAQ;QACfN;QACAC,OAAOlB,OAAOkC,SAASD,OAAO,CAAA,CAAE,IAAIA,OAAO,CAAA,IAAKd;MAClD,CAAA;IACF;EACF;AACA,SAAOU;AACT;AA7BSd;AAgCT,SAASO,iBAAiBc,KAAW;AACnC,MAAI;AACF,UAAMC,SAAkB/B,KAAKgC,MAAMF,GAAAA;AACnC,QACE,OAAOC,WAAW,YAClBA,WAAW,QACX,UAAUA,UACV,aAAaA,UACb,OAAOA,OAAOE,SAAS,YACvB,OAAOF,OAAOG,YAAY,UAC1B;AACA,aAAO;QAAED,MAAMF,OAAOE;QAAMC,SAASH,OAAOG;MAAQ;IACtD;EACF,QAAQ;EAER;AACA,SAAO;IAAED,MAAM;IAAiBC,SAASJ;EAAI;AAC/C;AAjBSd;","names":["AgentStreamError","CHUNK_FIELD","END_FIELD","FAIL_FIELD","RedisTokenStreamSink","keyPrefix","ttlSeconds","blockMs","redis","options","key","runId","open","write","chunk","xadd","Buffer","from","end","expire","fail","error","JSON","stringify","subscribe","reader","duplicate","lastId","response","xreadBuffer","entry","readEntries","id","field","value","undefined","Uint8Array","AgentStreamError","parseStreamError","toString","disconnect","close","del","Array","isArray","entries","stream","item","length","fields","isBuffer","push","raw","parsed","parse","code","message"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dudousxd/nestjs-agent",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Governed, durable-backed AI agent for NestJS — @AiTool, discovery, /agent controllers",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Davide Carvalho",
|
|
@@ -28,6 +28,16 @@
|
|
|
28
28
|
"types": "./dist/durable/index.d.cts",
|
|
29
29
|
"default": "./dist/durable/index.cjs"
|
|
30
30
|
}
|
|
31
|
+
},
|
|
32
|
+
"./sink-redis": {
|
|
33
|
+
"import": {
|
|
34
|
+
"types": "./dist/sink-redis/index.d.ts",
|
|
35
|
+
"default": "./dist/sink-redis/index.js"
|
|
36
|
+
},
|
|
37
|
+
"require": {
|
|
38
|
+
"types": "./dist/sink-redis/index.d.cts",
|
|
39
|
+
"default": "./dist/sink-redis/index.cjs"
|
|
40
|
+
}
|
|
31
41
|
}
|
|
32
42
|
},
|
|
33
43
|
"files": [
|
|
@@ -36,13 +46,14 @@
|
|
|
36
46
|
"dependencies": {
|
|
37
47
|
"@standard-schema/spec": "1.1.0",
|
|
38
48
|
"zod": "3.25.76",
|
|
39
|
-
"@dudousxd/nestjs-agent-core": "0.3.
|
|
49
|
+
"@dudousxd/nestjs-agent-core": "0.3.3"
|
|
40
50
|
},
|
|
41
51
|
"peerDependencies": {
|
|
42
|
-
"@nestjs/common": "^10.0.0 || ^11.0.0",
|
|
43
|
-
"@nestjs/core": "^10.0.0 || ^11.0.0",
|
|
44
52
|
"@dudousxd/nestjs-durable": ">=0.26.0",
|
|
45
53
|
"@dudousxd/nestjs-durable-core": ">=0.45.0",
|
|
54
|
+
"@nestjs/common": "^10.0.0 || ^11.0.0",
|
|
55
|
+
"@nestjs/core": "^10.0.0 || ^11.0.0",
|
|
56
|
+
"ioredis": "^5.0.0",
|
|
46
57
|
"reflect-metadata": "^0.2.0"
|
|
47
58
|
},
|
|
48
59
|
"peerDependenciesMeta": {
|
|
@@ -51,6 +62,9 @@
|
|
|
51
62
|
},
|
|
52
63
|
"@dudousxd/nestjs-durable-core": {
|
|
53
64
|
"optional": true
|
|
65
|
+
},
|
|
66
|
+
"ioredis": {
|
|
67
|
+
"optional": true
|
|
54
68
|
}
|
|
55
69
|
},
|
|
56
70
|
"devDependencies": {
|
|
@@ -65,13 +79,14 @@
|
|
|
65
79
|
"@nestjs/testing": "11.1.6",
|
|
66
80
|
"@types/express": "5.0.0",
|
|
67
81
|
"@types/supertest": "6.0.2",
|
|
82
|
+
"ioredis": "^5.4.1",
|
|
68
83
|
"reflect-metadata": "0.2.2",
|
|
69
84
|
"rxjs": "7.8.1",
|
|
70
85
|
"supertest": "7.0.0",
|
|
71
86
|
"tsup": "8.3.5",
|
|
72
87
|
"typescript": "5.9.3",
|
|
73
88
|
"zod": "3.25.76",
|
|
74
|
-
"@dudousxd/nestjs-agent-testing": "0.3.
|
|
89
|
+
"@dudousxd/nestjs-agent-testing": "0.3.3"
|
|
75
90
|
},
|
|
76
91
|
"scripts": {
|
|
77
92
|
"build": "tsup",
|