@dudousxd/nestjs-agent 1.34.0 → 1.35.1
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/a2ui/index.d.cts +2 -2
- package/dist/a2ui/index.d.ts +2 -2
- package/dist/{chat-queue.service-QIuTS-5c.d.cts → chat-queue.service-CF2fcJDx.d.cts} +5 -2
- package/dist/{chat-queue.service-wNWaoUN8.d.ts → chat-queue.service-CzXsKVb1.d.ts} +5 -2
- package/dist/durable/index.cjs +79 -9
- package/dist/durable/index.cjs.map +1 -1
- package/dist/durable/index.d.cts +1 -1
- package/dist/durable/index.d.ts +1 -1
- package/dist/durable/index.js +81 -11
- package/dist/durable/index.js.map +1 -1
- package/dist/{index-DFKUTFfZ.d.ts → index-DpS9P7so.d.ts} +1 -1
- package/dist/{index-DIFUEKPr.d.cts → index-NF93YDF4.d.cts} +1 -1
- package/dist/index.cjs +74 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +23 -6
- package/dist/index.d.ts +23 -6
- package/dist/index.js +75 -7
- package/dist/index.js.map +1 -1
- package/dist/sink-redis/index.cjs +13 -6
- package/dist/sink-redis/index.cjs.map +1 -1
- package/dist/sink-redis/index.js +13 -6
- package/dist/sink-redis/index.js.map +1 -1
- package/package.json +5 -5
|
@@ -94,16 +94,23 @@ var RedisTokenStreamSink = class {
|
|
|
94
94
|
await this.redis.del(this.key(runId));
|
|
95
95
|
}
|
|
96
96
|
};
|
|
97
|
-
function
|
|
98
|
-
if (
|
|
99
|
-
return [];
|
|
97
|
+
function streamEntryLists(response) {
|
|
98
|
+
if (Array.isArray(response)) {
|
|
99
|
+
return response.map((stream) => Array.isArray(stream) ? stream[1] : void 0);
|
|
100
|
+
}
|
|
101
|
+
if (typeof response === "object" && response !== null && !Buffer.isBuffer(response)) {
|
|
102
|
+
return Object.values(response);
|
|
100
103
|
}
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
__name(streamEntryLists, "streamEntryLists");
|
|
107
|
+
function readEntries(response) {
|
|
101
108
|
const entries = [];
|
|
102
|
-
for (const
|
|
103
|
-
if (!Array.isArray(
|
|
109
|
+
for (const list of streamEntryLists(response)) {
|
|
110
|
+
if (!Array.isArray(list)) {
|
|
104
111
|
continue;
|
|
105
112
|
}
|
|
106
|
-
for (const item of
|
|
113
|
+
for (const item of list) {
|
|
107
114
|
if (!Array.isArray(item) || item.length < 2) {
|
|
108
115
|
continue;
|
|
109
116
|
}
|
|
@@ -1 +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 * How long (seconds) a run's stream outlives its last write, so streams self-expire instead of\n * accumulating in Redis — ended runs past their replay window and runs that crashed without\n * ending alike (nothing calls `close()` on its own). A subscriber that reconnects within the window\n * still replays. Default 3600 (1h). `0` keeps the stream until `close()`.\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 await this.expire(key);\n },\n end: async () => {\n await this.redis.xadd(key, '*', END_FIELD, '1');\n await this.expire(key);\n },\n fail: async (error: StreamError) => {\n await this.redis.xadd(key, '*', FAIL_FIELD, JSON.stringify(error));\n await this.expire(key);\n },\n };\n }\n\n /** (Re)arm the TTL — sliding from the run's last write. `EXPIRE 0` would delete, so 0 skips. */\n private async expire(key: string): Promise<void> {\n if (this.ttlSeconds > 0) await this.redis.expire(key, this.ttlSeconds);\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","expire","end","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"]}
|
|
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 * How long (seconds) a run's stream outlives its last write, so streams self-expire instead of\n * accumulating in Redis — ended runs past their replay window and runs that crashed without\n * ending alike (nothing calls `close()` on its own). A subscriber that reconnects within the window\n * still replays. Default 3600 (1h). `0` keeps the stream until `close()`.\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 await this.expire(key);\n },\n end: async () => {\n await this.redis.xadd(key, '*', END_FIELD, '1');\n await this.expire(key);\n },\n fail: async (error: StreamError) => {\n await this.redis.xadd(key, '*', FAIL_FIELD, JSON.stringify(error));\n await this.expire(key);\n },\n };\n }\n\n /** (Re)arm the TTL — sliding from the run's last write. `EXPIRE 0` would delete, so 0 skips. */\n private async expire(key: string): Promise<void> {\n if (this.ttlSeconds > 0) await this.redis.expire(key, this.ttlSeconds);\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 * The per-stream entry lists of an `XREAD` reply. ioredis 5 (and ioredis 6 under its default\n * `replyMapping: \"legacy\"`) returns `[[key, entries], …]`; ioredis 6 with `replyMapping: \"resp3\"`\n * surfaces the RESP3 map as a plain object `{ [key]: entries }`. Accept both, so the sink works with\n * whatever client the app hands it.\n */\nfunction streamEntryLists(response: unknown): unknown[] {\n if (Array.isArray(response)) {\n return response.map((stream) => (Array.isArray(stream) ? stream[1] : undefined));\n }\n if (typeof response === 'object' && response !== null && !Buffer.isBuffer(response)) {\n return Object.values(response);\n }\n return [];\n}\n\n/**\n * Flatten ioredis's `XREAD` buffer response — `[[key, [[id, [field, value, …]], …]], …] | null`,\n * or the RESP3 map form (see {@link streamEntryLists}) — into the entries we care about. Validated\n * defensively at runtime (it crosses the Redis boundary) rather than trusting the driver's loose\n * typing.\n */\nfunction readEntries(response: unknown): StreamEntry[] {\n const entries: StreamEntry[] = [];\n for (const list of streamEntryLists(response)) {\n if (!Array.isArray(list)) {\n continue;\n }\n for (const item of list) {\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","expire","end","fail","error","JSON","stringify","subscribe","reader","duplicate","lastId","response","xreadBuffer","entry","readEntries","id","field","value","undefined","Uint8Array","AgentStreamError","parseStreamError","toString","disconnect","close","del","streamEntryLists","Array","isArray","map","stream","isBuffer","Object","values","entries","list","item","length","fields","push","raw","parsed","parse","code","message"]}
|
package/dist/sink-redis/index.js
CHANGED
|
@@ -70,16 +70,23 @@ var RedisTokenStreamSink = class {
|
|
|
70
70
|
await this.redis.del(this.key(runId));
|
|
71
71
|
}
|
|
72
72
|
};
|
|
73
|
-
function
|
|
74
|
-
if (
|
|
75
|
-
return [];
|
|
73
|
+
function streamEntryLists(response) {
|
|
74
|
+
if (Array.isArray(response)) {
|
|
75
|
+
return response.map((stream) => Array.isArray(stream) ? stream[1] : void 0);
|
|
76
|
+
}
|
|
77
|
+
if (typeof response === "object" && response !== null && !Buffer.isBuffer(response)) {
|
|
78
|
+
return Object.values(response);
|
|
76
79
|
}
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
__name(streamEntryLists, "streamEntryLists");
|
|
83
|
+
function readEntries(response) {
|
|
77
84
|
const entries = [];
|
|
78
|
-
for (const
|
|
79
|
-
if (!Array.isArray(
|
|
85
|
+
for (const list of streamEntryLists(response)) {
|
|
86
|
+
if (!Array.isArray(list)) {
|
|
80
87
|
continue;
|
|
81
88
|
}
|
|
82
|
-
for (const item of
|
|
89
|
+
for (const item of list) {
|
|
83
90
|
if (!Array.isArray(item) || item.length < 2) {
|
|
84
91
|
continue;
|
|
85
92
|
}
|
|
@@ -1 +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 * How long (seconds) a run's stream outlives its last write, so streams self-expire instead of\n * accumulating in Redis — ended runs past their replay window and runs that crashed without\n * ending alike (nothing calls `close()` on its own). A subscriber that reconnects within the window\n * still replays. Default 3600 (1h). `0` keeps the stream until `close()`.\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 await this.expire(key);\n },\n end: async () => {\n await this.redis.xadd(key, '*', END_FIELD, '1');\n await this.expire(key);\n },\n fail: async (error: StreamError) => {\n await this.redis.xadd(key, '*', FAIL_FIELD, JSON.stringify(error));\n await this.expire(key);\n },\n };\n }\n\n /** (Re)arm the TTL — sliding from the run's last write. `EXPIRE 0` would delete, so 0 skips. */\n private async expire(key: string): Promise<void> {\n if (this.ttlSeconds > 0) await this.redis.expire(key, this.ttlSeconds);\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":["AgentStreamError","CHUNK_FIELD","END_FIELD","FAIL_FIELD","RedisTokenStreamSink","keyPrefix","ttlSeconds","blockMs","redis","options","key","runId","open","write","chunk","xadd","Buffer","from","expire","end","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"]}
|
|
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 * How long (seconds) a run's stream outlives its last write, so streams self-expire instead of\n * accumulating in Redis — ended runs past their replay window and runs that crashed without\n * ending alike (nothing calls `close()` on its own). A subscriber that reconnects within the window\n * still replays. Default 3600 (1h). `0` keeps the stream until `close()`.\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 await this.expire(key);\n },\n end: async () => {\n await this.redis.xadd(key, '*', END_FIELD, '1');\n await this.expire(key);\n },\n fail: async (error: StreamError) => {\n await this.redis.xadd(key, '*', FAIL_FIELD, JSON.stringify(error));\n await this.expire(key);\n },\n };\n }\n\n /** (Re)arm the TTL — sliding from the run's last write. `EXPIRE 0` would delete, so 0 skips. */\n private async expire(key: string): Promise<void> {\n if (this.ttlSeconds > 0) await this.redis.expire(key, this.ttlSeconds);\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 * The per-stream entry lists of an `XREAD` reply. ioredis 5 (and ioredis 6 under its default\n * `replyMapping: \"legacy\"`) returns `[[key, entries], …]`; ioredis 6 with `replyMapping: \"resp3\"`\n * surfaces the RESP3 map as a plain object `{ [key]: entries }`. Accept both, so the sink works with\n * whatever client the app hands it.\n */\nfunction streamEntryLists(response: unknown): unknown[] {\n if (Array.isArray(response)) {\n return response.map((stream) => (Array.isArray(stream) ? stream[1] : undefined));\n }\n if (typeof response === 'object' && response !== null && !Buffer.isBuffer(response)) {\n return Object.values(response);\n }\n return [];\n}\n\n/**\n * Flatten ioredis's `XREAD` buffer response — `[[key, [[id, [field, value, …]], …]], …] | null`,\n * or the RESP3 map form (see {@link streamEntryLists}) — into the entries we care about. Validated\n * defensively at runtime (it crosses the Redis boundary) rather than trusting the driver's loose\n * typing.\n */\nfunction readEntries(response: unknown): StreamEntry[] {\n const entries: StreamEntry[] = [];\n for (const list of streamEntryLists(response)) {\n if (!Array.isArray(list)) {\n continue;\n }\n for (const item of list) {\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":["AgentStreamError","CHUNK_FIELD","END_FIELD","FAIL_FIELD","RedisTokenStreamSink","keyPrefix","ttlSeconds","blockMs","redis","options","key","runId","open","write","chunk","xadd","Buffer","from","expire","end","fail","error","JSON","stringify","subscribe","reader","duplicate","lastId","response","xreadBuffer","entry","readEntries","id","field","value","undefined","Uint8Array","AgentStreamError","parseStreamError","toString","disconnect","close","del","streamEntryLists","Array","isArray","map","stream","isBuffer","Object","values","entries","list","item","length","fields","push","raw","parsed","parse","code","message"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dudousxd/nestjs-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.35.1",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/DavideCarvalho/nestjs-agent.git",
|
|
@@ -110,7 +110,7 @@
|
|
|
110
110
|
"dependencies": {
|
|
111
111
|
"@standard-schema/spec": "1.1.0",
|
|
112
112
|
"zod": "3.25.76",
|
|
113
|
-
"@dudousxd/nestjs-agent-core": "0.
|
|
113
|
+
"@dudousxd/nestjs-agent-core": "0.57.0"
|
|
114
114
|
},
|
|
115
115
|
"peerDependencies": {
|
|
116
116
|
"@dudousxd/nestjs-durable": ">=0.34.0",
|
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
"@nestjs/common": "^10.0.0 || ^11.0.0 || ^12.0.0",
|
|
120
120
|
"@nestjs/core": "^10.0.0 || ^11.0.0 || ^12.0.0",
|
|
121
121
|
"@nestjs/platform-express": "^10.0.0 || ^11.0.0 || ^12.0.0",
|
|
122
|
-
"ioredis": "^5.0.0",
|
|
122
|
+
"ioredis": "^5.0.0 || ^6.0.0",
|
|
123
123
|
"reflect-metadata": "^0.2.0",
|
|
124
124
|
"vite": ">=5"
|
|
125
125
|
},
|
|
@@ -161,7 +161,7 @@
|
|
|
161
161
|
"@types/multer": "1.4.13",
|
|
162
162
|
"@types/supertest": "6.0.3",
|
|
163
163
|
"ajv": "8.20.0",
|
|
164
|
-
"ioredis": "
|
|
164
|
+
"ioredis": "6.0.0",
|
|
165
165
|
"nestjs-agent-before": "npm:@dudousxd/nestjs-agent@1.19.3",
|
|
166
166
|
"reflect-metadata": "0.2.2",
|
|
167
167
|
"rxjs": "7.8.2",
|
|
@@ -169,7 +169,7 @@
|
|
|
169
169
|
"tsup": "8.5.1",
|
|
170
170
|
"typescript": "5.9.3",
|
|
171
171
|
"zod": "3.25.76",
|
|
172
|
-
"@dudousxd/nestjs-agent-testing": "0.
|
|
172
|
+
"@dudousxd/nestjs-agent-testing": "0.30.0"
|
|
173
173
|
},
|
|
174
174
|
"scripts": {
|
|
175
175
|
"build": "tsup",
|