@dudousxd/nestjs-agent 1.21.0 → 1.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -52,17 +52,22 @@ var RedisTokenStreamSink = class {
52
52
  return {
53
53
  write: /* @__PURE__ */ __name(async (chunk) => {
54
54
  await this.redis.xadd(key, "*", CHUNK_FIELD, Buffer.from(chunk));
55
+ await this.expire(key);
55
56
  }, "write"),
56
57
  end: /* @__PURE__ */ __name(async () => {
57
58
  await this.redis.xadd(key, "*", END_FIELD, "1");
58
- await this.redis.expire(key, this.ttlSeconds);
59
+ await this.expire(key);
59
60
  }, "end"),
60
61
  fail: /* @__PURE__ */ __name(async (error) => {
61
62
  await this.redis.xadd(key, "*", FAIL_FIELD, JSON.stringify(error));
62
- await this.redis.expire(key, this.ttlSeconds);
63
+ await this.expire(key);
63
64
  }, "fail")
64
65
  };
65
66
  }
67
+ /** (Re)arm the TTL — sliding from the run's last write. `EXPIRE 0` would delete, so 0 skips. */
68
+ async expire(key) {
69
+ if (this.ttlSeconds > 0) await this.redis.expire(key, this.ttlSeconds);
70
+ }
66
71
  async *subscribe(runId) {
67
72
  const key = this.key(runId);
68
73
  const reader = this.redis.duplicate();
@@ -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 * 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"]}
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"]}
@@ -5,9 +5,10 @@ interface RedisTokenStreamSinkOptions {
5
5
  /** Key namespace for the per-run streams. Default `agent:sink:`. */
6
6
  keyPrefix?: string;
7
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).
8
+ * How long (seconds) a run's stream outlives its last write, so streams self-expire instead of
9
+ * accumulating in Redis — ended runs past their replay window and runs that crashed without
10
+ * ending alike (nothing calls `close()` on its own). A subscriber that reconnects within the window
11
+ * still replays. Default 3600 (1h). `0` keeps the stream until `close()`.
11
12
  */
12
13
  ttlSeconds?: number;
13
14
  /** `XREAD BLOCK` timeout (ms) between follow polls. Default 5000. Purely an internal cadence. */
@@ -32,6 +33,8 @@ declare class RedisTokenStreamSink implements TokenStreamSink {
32
33
  constructor(redis: Redis, options?: RedisTokenStreamSinkOptions);
33
34
  private key;
34
35
  open(runId: string): SinkWriter;
36
+ /** (Re)arm the TTL — sliding from the run's last write. `EXPIRE 0` would delete, so 0 skips. */
37
+ private expire;
35
38
  subscribe(runId: string): AsyncIterable<Uint8Array>;
36
39
  close(runId: string): Promise<void>;
37
40
  }
@@ -5,9 +5,10 @@ interface RedisTokenStreamSinkOptions {
5
5
  /** Key namespace for the per-run streams. Default `agent:sink:`. */
6
6
  keyPrefix?: string;
7
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).
8
+ * How long (seconds) a run's stream outlives its last write, so streams self-expire instead of
9
+ * accumulating in Redis — ended runs past their replay window and runs that crashed without
10
+ * ending alike (nothing calls `close()` on its own). A subscriber that reconnects within the window
11
+ * still replays. Default 3600 (1h). `0` keeps the stream until `close()`.
11
12
  */
12
13
  ttlSeconds?: number;
13
14
  /** `XREAD BLOCK` timeout (ms) between follow polls. Default 5000. Purely an internal cadence. */
@@ -32,6 +33,8 @@ declare class RedisTokenStreamSink implements TokenStreamSink {
32
33
  constructor(redis: Redis, options?: RedisTokenStreamSinkOptions);
33
34
  private key;
34
35
  open(runId: string): SinkWriter;
36
+ /** (Re)arm the TTL — sliding from the run's last write. `EXPIRE 0` would delete, so 0 skips. */
37
+ private expire;
35
38
  subscribe(runId: string): AsyncIterable<Uint8Array>;
36
39
  close(runId: string): Promise<void>;
37
40
  }
@@ -28,17 +28,22 @@ var RedisTokenStreamSink = class {
28
28
  return {
29
29
  write: /* @__PURE__ */ __name(async (chunk) => {
30
30
  await this.redis.xadd(key, "*", CHUNK_FIELD, Buffer.from(chunk));
31
+ await this.expire(key);
31
32
  }, "write"),
32
33
  end: /* @__PURE__ */ __name(async () => {
33
34
  await this.redis.xadd(key, "*", END_FIELD, "1");
34
- await this.redis.expire(key, this.ttlSeconds);
35
+ await this.expire(key);
35
36
  }, "end"),
36
37
  fail: /* @__PURE__ */ __name(async (error) => {
37
38
  await this.redis.xadd(key, "*", FAIL_FIELD, JSON.stringify(error));
38
- await this.redis.expire(key, this.ttlSeconds);
39
+ await this.expire(key);
39
40
  }, "fail")
40
41
  };
41
42
  }
43
+ /** (Re)arm the TTL — sliding from the run's last write. `EXPIRE 0` would delete, so 0 skips. */
44
+ async expire(key) {
45
+ if (this.ttlSeconds > 0) await this.redis.expire(key, this.ttlSeconds);
46
+ }
42
47
  async *subscribe(runId) {
43
48
  const key = this.key(runId);
44
49
  const reader = this.redis.duplicate();
@@ -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 * 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":["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"]}
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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dudousxd/nestjs-agent",
3
- "version": "1.21.0",
3
+ "version": "1.22.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/DavideCarvalho/nestjs-agent.git",
@@ -89,7 +89,7 @@
89
89
  "dependencies": {
90
90
  "@standard-schema/spec": "1.1.0",
91
91
  "zod": "3.25.76",
92
- "@dudousxd/nestjs-agent-core": "0.40.0"
92
+ "@dudousxd/nestjs-agent-core": "0.41.0"
93
93
  },
94
94
  "peerDependencies": {
95
95
  "@dudousxd/nestjs-durable": ">=0.34.0",
@@ -120,7 +120,7 @@
120
120
  "@ag-ui/core": "1.0.1",
121
121
  "@dudousxd/durable-worker": "0.10.1",
122
122
  "@dudousxd/nestjs-context": "0.2.1",
123
- "@dudousxd/nestjs-durable": "0.45.0",
123
+ "@dudousxd/nestjs-durable": "0.45.1",
124
124
  "@dudousxd/nestjs-durable-core": "0.72.1",
125
125
  "@dudousxd/nestjs-durable-transport-event-emitter": "0.5.1",
126
126
  "@dudousxd/nestjs-media": "0.9.3",
@@ -143,7 +143,7 @@
143
143
  "tsup": "8.5.1",
144
144
  "typescript": "5.9.3",
145
145
  "zod": "3.25.76",
146
- "@dudousxd/nestjs-agent-testing": "0.26.0"
146
+ "@dudousxd/nestjs-agent-testing": "0.26.1"
147
147
  },
148
148
  "scripts": {
149
149
  "build": "tsup",