@qjp88995/agent-runtime-redis 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/coordination.d.ts +97 -0
- package/dist/coordination.d.ts.map +1 -0
- package/dist/coordination.js +221 -0
- package/dist/coordination.js.map +1 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/keys.d.ts +21 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +12 -0
- package/dist/keys.js.map +1 -0
- package/dist/stream-bus.d.ts +80 -0
- package/dist/stream-bus.d.ts.map +1 -0
- package/dist/stream-bus.js +285 -0
- package/dist/stream-bus.js.map +1 -0
- package/package.json +46 -3
- package/src/coordination.ts +329 -0
- package/src/index.ts +77 -0
- package/src/keys.ts +32 -0
- package/src/stream-bus.ts +343 -0
- package/README.md +0 -3
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { Redis } from 'ioredis';
|
|
2
|
+
import { type LoggerFactory, type SessionLock, type SubagentAwaitLease, type TreeBudget, type WakeChannel } from '@qjp88995/agent-runtime';
|
|
3
|
+
import { type RedisKeyOptions } from './keys.js';
|
|
4
|
+
export interface RedisSessionLockOptions extends RedisKeyOptions {
|
|
5
|
+
/** 缺省 `LOCK_TTL_MS`(60s)。 */
|
|
6
|
+
ttlMs?: number;
|
|
7
|
+
}
|
|
8
|
+
/** SET NX PX 抢锁,Lua 比对 token 再 DEL / PEXPIRE。TTL 兜底持有者崩溃。 */
|
|
9
|
+
export declare class RedisSessionLock implements SessionLock {
|
|
10
|
+
private readonly redis;
|
|
11
|
+
private readonly keys;
|
|
12
|
+
private readonly ttlMs;
|
|
13
|
+
constructor(redis: Redis, options?: RedisSessionLockOptions);
|
|
14
|
+
acquire(sessionId: string): Promise<string | null>;
|
|
15
|
+
renew(sessionId: string, token: string): Promise<boolean>;
|
|
16
|
+
release(sessionId: string, token: string): Promise<void>;
|
|
17
|
+
isHeld(sessionId: string): Promise<boolean>;
|
|
18
|
+
}
|
|
19
|
+
export interface RedisWakeChannelOptions extends RedisKeyOptions {
|
|
20
|
+
/**
|
|
21
|
+
* 订阅用的连接。缺省 `redis.duplicate()`,由本对象负责关闭;宿主自己给的不关。
|
|
22
|
+
*/
|
|
23
|
+
subscriber?: Redis;
|
|
24
|
+
logger?: LoggerFactory;
|
|
25
|
+
}
|
|
26
|
+
type Handler = (sessionId: string) => void;
|
|
27
|
+
/**
|
|
28
|
+
* 唤醒 / 空闲广播走 Redis pub/sub(at-most-once,订阅前发的收不到——与端口约定一致)。
|
|
29
|
+
*
|
|
30
|
+
* 用 `RedisWakeChannel.create()` 构造:它等 SUBSCRIBE 应答回来才返回,之后 `onWake` / `onIdle`
|
|
31
|
+
* 只是登记本地处理函数,同步生效。否则「刚订阅就有人 wake」会落在 SUBSCRIBE 应答之前、静默丢失。
|
|
32
|
+
* 断线重连后 ioredis 会自动重新订阅(`autoResubscribe` 缺省开);断线期间的广播照样丢,
|
|
33
|
+
* 这由 sweeper 的启动补唤醒与消费方自己的兜底扫描覆盖。
|
|
34
|
+
*
|
|
35
|
+
* 一个处理函数抛错只记日志,不影响别的处理函数,也不冒进 ioredis 的事件循环。
|
|
36
|
+
*/
|
|
37
|
+
export declare class RedisWakeChannel implements WakeChannel {
|
|
38
|
+
private readonly redis;
|
|
39
|
+
private readonly sub;
|
|
40
|
+
private readonly ownsSub;
|
|
41
|
+
private readonly keys;
|
|
42
|
+
private readonly logger;
|
|
43
|
+
private readonly wakeHandlers;
|
|
44
|
+
private readonly idleHandlers;
|
|
45
|
+
private disposed;
|
|
46
|
+
private constructor();
|
|
47
|
+
static create(redis: Redis, options?: RedisWakeChannelOptions): Promise<RedisWakeChannel>;
|
|
48
|
+
private dispatch;
|
|
49
|
+
private fanOut;
|
|
50
|
+
wake(sessionId: string): Promise<void>;
|
|
51
|
+
onWake(handler: Handler): () => void;
|
|
52
|
+
idle(sessionId: string): Promise<void>;
|
|
53
|
+
onIdle(handler: Handler): () => void;
|
|
54
|
+
/** 摘掉所有处理函数;订阅连接是本对象建的就关掉。 */
|
|
55
|
+
dispose(): Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
export interface RedisSubagentAwaitLeaseOptions extends RedisKeyOptions {
|
|
58
|
+
/** 缺省 `AWAIT_LEASE_TTL_MS`(60s)。 */
|
|
59
|
+
ttlMs?: number;
|
|
60
|
+
logger?: LoggerFactory;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 等待租约:一个带 TTL 的键。`claim` 是一条原子 DEL,父与 finalizer 抢同一条,恰有一方胜出。
|
|
64
|
+
* Redis 出错时按端口约定一律退化成异步(见各方法)。
|
|
65
|
+
*/
|
|
66
|
+
export declare class RedisSubagentAwaitLease implements SubagentAwaitLease {
|
|
67
|
+
private readonly redis;
|
|
68
|
+
private readonly keys;
|
|
69
|
+
private readonly ttlMs;
|
|
70
|
+
private readonly logger;
|
|
71
|
+
constructor(redis: Redis, options?: RedisSubagentAwaitLeaseOptions);
|
|
72
|
+
/** 失败返回 false:没有租约就没有互斥,调用方退回异步模式。 */
|
|
73
|
+
begin(childSessionId: string): Promise<boolean>;
|
|
74
|
+
/** 只续已存在的租约(PEXPIRE 对不存在的键是 no-op)。失败不抛。 */
|
|
75
|
+
renew(childSessionId: string): Promise<void>;
|
|
76
|
+
/** 拿不准当作没人等:猜「有人等」会让该回投的产出被跳过,那是静默丢失。 */
|
|
77
|
+
isAwaited(childSessionId: string): Promise<boolean>;
|
|
78
|
+
/** 拿不准时父得 true(放弃等待)、finalizer 得 false(照常回投),合起来就是退化成异步。 */
|
|
79
|
+
claim(childSessionId: string, by: 'parent' | 'finalizer'): Promise<boolean>;
|
|
80
|
+
}
|
|
81
|
+
export interface RedisTreeBudgetOptions extends RedisKeyOptions {
|
|
82
|
+
/** 计数键的 TTL,兜底「整棵树都没了、计数还留着」。缺省 1 天。 */
|
|
83
|
+
ttlSeconds?: number;
|
|
84
|
+
}
|
|
85
|
+
/** 树级名额:Lua 里做「读比较写」,并发 `tryIncr` 不超发。 */
|
|
86
|
+
export declare class RedisTreeBudget implements TreeBudget {
|
|
87
|
+
private readonly redis;
|
|
88
|
+
private readonly keys;
|
|
89
|
+
private readonly ttlSeconds;
|
|
90
|
+
constructor(redis: Redis, options?: RedisTreeBudgetOptions);
|
|
91
|
+
tryIncr(rootSessionId: string, limit: number): Promise<boolean>;
|
|
92
|
+
decr(rootSessionId: string): Promise<void>;
|
|
93
|
+
/** 与进程内实现一致:n ≤ 0 直接删键。 */
|
|
94
|
+
set(rootSessionId: string, n: number): Promise<void>;
|
|
95
|
+
}
|
|
96
|
+
export {};
|
|
97
|
+
//# sourceMappingURL=coordination.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"coordination.d.ts","sourceRoot":"","sources":["../src/coordination.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { AWAIT_LEASE_TTL_MS, consoleLogger, LOCK_TTL_MS, } from '@qjp88995/agent-runtime';
|
|
3
|
+
import { redisKeys } from './keys.js';
|
|
4
|
+
/**
|
|
5
|
+
* 会话锁、唤醒 / 空闲广播、子 Agent 等待租约、树级名额的 Redis 实现。
|
|
6
|
+
* 搬自 agent-base `agent-core/loop/{session-lock,session-wake}.service.ts`、`session-idle.ts`、
|
|
7
|
+
* `agent-core/subagent/{subagent-await,tree-budget}.service.ts`,语义见 runtime 的 `ports/coordination`。
|
|
8
|
+
*
|
|
9
|
+
* 锁 / 租约 / 名额都直接用宿主给的共享连接(全是非阻塞的常规命令),只有唤醒频道要一条
|
|
10
|
+
* 独立的订阅连接(进了 subscribe 模式的连接不能再发普通命令)。
|
|
11
|
+
*/
|
|
12
|
+
/** DEL 前比对 token,避免误删「TTL 过期后被别人重新抢到」的锁。 */
|
|
13
|
+
const RELEASE_LUA = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
|
|
14
|
+
/** PEXPIRE 前比对 token,只续自己的锁。 */
|
|
15
|
+
const RENEW_LUA = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('pexpire', KEYS[1], ARGV[2]) else return 0 end";
|
|
16
|
+
/** SET NX PX 抢锁,Lua 比对 token 再 DEL / PEXPIRE。TTL 兜底持有者崩溃。 */
|
|
17
|
+
export class RedisSessionLock {
|
|
18
|
+
redis;
|
|
19
|
+
keys;
|
|
20
|
+
ttlMs;
|
|
21
|
+
constructor(redis, options = {}) {
|
|
22
|
+
this.redis = redis;
|
|
23
|
+
this.keys = redisKeys(options);
|
|
24
|
+
this.ttlMs = options.ttlMs ?? LOCK_TTL_MS;
|
|
25
|
+
}
|
|
26
|
+
async acquire(sessionId) {
|
|
27
|
+
const token = randomUUID();
|
|
28
|
+
const res = await this.redis.set(this.keys.lock(sessionId), token, 'PX', this.ttlMs, 'NX');
|
|
29
|
+
return res === 'OK' ? token : null;
|
|
30
|
+
}
|
|
31
|
+
async renew(sessionId, token) {
|
|
32
|
+
const res = await this.redis.eval(RENEW_LUA, 1, this.keys.lock(sessionId), token, String(this.ttlMs));
|
|
33
|
+
return res === 1;
|
|
34
|
+
}
|
|
35
|
+
async release(sessionId, token) {
|
|
36
|
+
await this.redis.eval(RELEASE_LUA, 1, this.keys.lock(sessionId), token);
|
|
37
|
+
}
|
|
38
|
+
async isHeld(sessionId) {
|
|
39
|
+
return (await this.redis.exists(this.keys.lock(sessionId))) === 1;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 唤醒 / 空闲广播走 Redis pub/sub(at-most-once,订阅前发的收不到——与端口约定一致)。
|
|
44
|
+
*
|
|
45
|
+
* 用 `RedisWakeChannel.create()` 构造:它等 SUBSCRIBE 应答回来才返回,之后 `onWake` / `onIdle`
|
|
46
|
+
* 只是登记本地处理函数,同步生效。否则「刚订阅就有人 wake」会落在 SUBSCRIBE 应答之前、静默丢失。
|
|
47
|
+
* 断线重连后 ioredis 会自动重新订阅(`autoResubscribe` 缺省开);断线期间的广播照样丢,
|
|
48
|
+
* 这由 sweeper 的启动补唤醒与消费方自己的兜底扫描覆盖。
|
|
49
|
+
*
|
|
50
|
+
* 一个处理函数抛错只记日志,不影响别的处理函数,也不冒进 ioredis 的事件循环。
|
|
51
|
+
*/
|
|
52
|
+
export class RedisWakeChannel {
|
|
53
|
+
redis;
|
|
54
|
+
sub;
|
|
55
|
+
ownsSub;
|
|
56
|
+
keys;
|
|
57
|
+
logger;
|
|
58
|
+
wakeHandlers = new Set();
|
|
59
|
+
idleHandlers = new Set();
|
|
60
|
+
disposed = false;
|
|
61
|
+
constructor(redis, sub, ownsSub, keys, logger) {
|
|
62
|
+
this.redis = redis;
|
|
63
|
+
this.sub = sub;
|
|
64
|
+
this.ownsSub = ownsSub;
|
|
65
|
+
this.keys = keys;
|
|
66
|
+
this.logger = logger;
|
|
67
|
+
}
|
|
68
|
+
static async create(redis, options = {}) {
|
|
69
|
+
const sub = options.subscriber ?? redis.duplicate();
|
|
70
|
+
const channel = new RedisWakeChannel(redis, sub, !options.subscriber, redisKeys(options), (options.logger ?? consoleLogger)('RedisWakeChannel'));
|
|
71
|
+
sub.on('message', (ch, message) => channel.dispatch(ch, message));
|
|
72
|
+
await sub.subscribe(channel.keys.wakeChannel, channel.keys.idleChannel);
|
|
73
|
+
return channel;
|
|
74
|
+
}
|
|
75
|
+
dispatch(ch, message) {
|
|
76
|
+
if (ch === this.keys.wakeChannel) {
|
|
77
|
+
this.fanOut(this.wakeHandlers, message, 'wake');
|
|
78
|
+
}
|
|
79
|
+
else if (ch === this.keys.idleChannel) {
|
|
80
|
+
let sessionId;
|
|
81
|
+
try {
|
|
82
|
+
sessionId = JSON.parse(message).sessionId;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
sessionId = undefined;
|
|
86
|
+
}
|
|
87
|
+
if (typeof sessionId !== 'string') {
|
|
88
|
+
this.logger.warn(`dropping malformed idle message: ${message}`);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
this.fanOut(this.idleHandlers, sessionId, 'idle');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
fanOut(handlers, sessionId, kind) {
|
|
95
|
+
for (const handler of [...handlers]) {
|
|
96
|
+
try {
|
|
97
|
+
handler(sessionId);
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
this.logger.error(`${kind} handler failed for ${sessionId}: ${String(err)}`, err instanceof Error ? err.stack : undefined);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async wake(sessionId) {
|
|
105
|
+
await this.redis.publish(this.keys.wakeChannel, sessionId);
|
|
106
|
+
}
|
|
107
|
+
onWake(handler) {
|
|
108
|
+
this.wakeHandlers.add(handler);
|
|
109
|
+
return () => this.wakeHandlers.delete(handler);
|
|
110
|
+
}
|
|
111
|
+
async idle(sessionId) {
|
|
112
|
+
await this.redis.publish(this.keys.idleChannel, JSON.stringify({ sessionId }));
|
|
113
|
+
}
|
|
114
|
+
onIdle(handler) {
|
|
115
|
+
this.idleHandlers.add(handler);
|
|
116
|
+
return () => this.idleHandlers.delete(handler);
|
|
117
|
+
}
|
|
118
|
+
/** 摘掉所有处理函数;订阅连接是本对象建的就关掉。 */
|
|
119
|
+
async dispose() {
|
|
120
|
+
if (this.disposed)
|
|
121
|
+
return;
|
|
122
|
+
this.disposed = true;
|
|
123
|
+
this.wakeHandlers.clear();
|
|
124
|
+
this.idleHandlers.clear();
|
|
125
|
+
if (this.ownsSub) {
|
|
126
|
+
await this.sub.quit().catch(() => undefined);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
await this.sub
|
|
130
|
+
.unsubscribe(this.keys.wakeChannel, this.keys.idleChannel)
|
|
131
|
+
.catch(() => undefined);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* 等待租约:一个带 TTL 的键。`claim` 是一条原子 DEL,父与 finalizer 抢同一条,恰有一方胜出。
|
|
137
|
+
* Redis 出错时按端口约定一律退化成异步(见各方法)。
|
|
138
|
+
*/
|
|
139
|
+
export class RedisSubagentAwaitLease {
|
|
140
|
+
redis;
|
|
141
|
+
keys;
|
|
142
|
+
ttlMs;
|
|
143
|
+
logger;
|
|
144
|
+
constructor(redis, options = {}) {
|
|
145
|
+
this.redis = redis;
|
|
146
|
+
this.keys = redisKeys(options);
|
|
147
|
+
this.ttlMs = options.ttlMs ?? AWAIT_LEASE_TTL_MS;
|
|
148
|
+
this.logger = (options.logger ?? consoleLogger)('RedisSubagentAwaitLease');
|
|
149
|
+
}
|
|
150
|
+
/** 失败返回 false:没有租约就没有互斥,调用方退回异步模式。 */
|
|
151
|
+
async begin(childSessionId) {
|
|
152
|
+
try {
|
|
153
|
+
await this.redis.set(this.keys.awaited(childSessionId), '1', 'PX', this.ttlMs);
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
this.logger.warn(`await lease begin(${childSessionId}) failed, falling back to async: ${String(err)}`);
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/** 只续已存在的租约(PEXPIRE 对不存在的键是 no-op)。失败不抛。 */
|
|
162
|
+
async renew(childSessionId) {
|
|
163
|
+
try {
|
|
164
|
+
await this.redis.pexpire(this.keys.awaited(childSessionId), this.ttlMs);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// 续租失败不打断等待:最坏是租约过期、finalizer 改走回投,父随后超时降级。
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/** 拿不准当作没人等:猜「有人等」会让该回投的产出被跳过,那是静默丢失。 */
|
|
171
|
+
async isAwaited(childSessionId) {
|
|
172
|
+
try {
|
|
173
|
+
return (await this.redis.exists(this.keys.awaited(childSessionId))) === 1;
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
this.logger.warn(`await lease isAwaited(${childSessionId}) failed, assuming none: ${String(err)}`);
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/** 拿不准时父得 true(放弃等待)、finalizer 得 false(照常回投),合起来就是退化成异步。 */
|
|
181
|
+
async claim(childSessionId, by) {
|
|
182
|
+
try {
|
|
183
|
+
return (await this.redis.del(this.keys.awaited(childSessionId))) === 1;
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
this.logger.warn(`await lease claim(${childSessionId}, ${by}) failed: ${String(err)}`);
|
|
187
|
+
return by === 'parent';
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/** 只有当前值 < 上限才 INCR(并刷新 TTL),否则不动。 */
|
|
192
|
+
const INCR_IF_UNDER_LUA = "local v=tonumber(redis.call('get',KEYS[1]) or '0') if v<tonumber(ARGV[1]) then redis.call('incr',KEYS[1]) redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end";
|
|
193
|
+
/** DECR 后 ≤ 0 就 DEL,不留负值。 */
|
|
194
|
+
const DECR_LUA = "local v=redis.call('decr',KEYS[1]) if v<=0 then redis.call('del',KEYS[1]) end return v";
|
|
195
|
+
/** 树级名额:Lua 里做「读比较写」,并发 `tryIncr` 不超发。 */
|
|
196
|
+
export class RedisTreeBudget {
|
|
197
|
+
redis;
|
|
198
|
+
keys;
|
|
199
|
+
ttlSeconds;
|
|
200
|
+
constructor(redis, options = {}) {
|
|
201
|
+
this.redis = redis;
|
|
202
|
+
this.keys = redisKeys(options);
|
|
203
|
+
this.ttlSeconds = options.ttlSeconds ?? 86_400;
|
|
204
|
+
}
|
|
205
|
+
async tryIncr(rootSessionId, limit) {
|
|
206
|
+
const res = await this.redis.eval(INCR_IF_UNDER_LUA, 1, this.keys.liveSubs(rootSessionId), String(limit), String(this.ttlSeconds));
|
|
207
|
+
return res === 1;
|
|
208
|
+
}
|
|
209
|
+
async decr(rootSessionId) {
|
|
210
|
+
await this.redis.eval(DECR_LUA, 1, this.keys.liveSubs(rootSessionId));
|
|
211
|
+
}
|
|
212
|
+
/** 与进程内实现一致:n ≤ 0 直接删键。 */
|
|
213
|
+
async set(rootSessionId, n) {
|
|
214
|
+
const key = this.keys.liveSubs(rootSessionId);
|
|
215
|
+
if (n <= 0)
|
|
216
|
+
await this.redis.del(key);
|
|
217
|
+
else
|
|
218
|
+
await this.redis.set(key, String(n), 'EX', this.ttlSeconds);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
//# sourceMappingURL=coordination.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"coordination.js","sourceRoot":"","sources":["../src/coordination.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@qjp88995/agent-runtime` 的 Redis 适配:多实例部署时替换进程内的总线与协调件。
|
|
3
|
+
*
|
|
4
|
+
* 换的是五个端口——`StreamBus`、`SessionLock`、`WakeChannel`、`SubagentAwaitLease`、`TreeBudget`。
|
|
5
|
+
* 存储类端口(跑次、消息、会话、问题、厂商文件)不在这里:它们归宿主的数据库。
|
|
6
|
+
*/
|
|
7
|
+
import type { Redis } from 'ioredis';
|
|
8
|
+
import type { LoggerFactory } from '@qjp88995/agent-runtime';
|
|
9
|
+
import { RedisSessionLock, RedisSubagentAwaitLease, RedisTreeBudget, RedisWakeChannel } from './coordination.js';
|
|
10
|
+
import type { RedisKeyOptions } from './keys.js';
|
|
11
|
+
import { RedisStreamBus, type RedisStreamBusOptions } from './stream-bus.js';
|
|
12
|
+
export { RedisSessionLock, type RedisSessionLockOptions, RedisSubagentAwaitLease, type RedisSubagentAwaitLeaseOptions, RedisTreeBudget, type RedisTreeBudgetOptions, RedisWakeChannel, type RedisWakeChannelOptions, } from './coordination.js';
|
|
13
|
+
export { type RedisKeyOptions, type RedisKeys, redisKeys } from './keys.js';
|
|
14
|
+
export { RedisStreamBus, type RedisStreamBusOptions } from './stream-bus.js';
|
|
15
|
+
export interface RedisPortsOptions extends RedisKeyOptions {
|
|
16
|
+
logger?: LoggerFactory;
|
|
17
|
+
/** 透传给 `RedisStreamBus`(`keyPrefix` 以外层的为准)。 */
|
|
18
|
+
bus?: Omit<RedisStreamBusOptions, 'keyPrefix'>;
|
|
19
|
+
}
|
|
20
|
+
export interface RedisPorts {
|
|
21
|
+
bus: RedisStreamBus;
|
|
22
|
+
lock: RedisSessionLock;
|
|
23
|
+
wake: RedisWakeChannel;
|
|
24
|
+
awaitLease: RedisSubagentAwaitLease;
|
|
25
|
+
treeBudget: RedisTreeBudget;
|
|
26
|
+
/** 停总线定时器、关唤醒频道的订阅连接。不关 `redis` 本身(它是宿主的)。 */
|
|
27
|
+
dispose(): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 一组 Redis 实现,键名与 `createInProcessPorts()` 的对应项相同,宿主可以直接覆盖:
|
|
31
|
+
*
|
|
32
|
+
* ```ts
|
|
33
|
+
* const ports = { ...createInProcessPorts(), ...(await createRedisPorts(redis)) };
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* `redis` 是宿主的共享连接(锁、租约、名额、写会话流都用它);唤醒频道与每个会话订阅
|
|
37
|
+
* 各自 `duplicate()` 一条。等唤醒频道订阅就绪才返回。
|
|
38
|
+
*/
|
|
39
|
+
export declare function createRedisPorts(redis: Redis, options?: RedisPortsOptions): Promise<RedisPorts>;
|
|
40
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAErC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAE7D,OAAO,EACL,gBAAgB,EAChB,uBAAuB,EACvB,eAAe,EACf,gBAAgB,EACjB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,KAAK,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAE7E,OAAO,EACL,gBAAgB,EAChB,KAAK,uBAAuB,EAC5B,uBAAuB,EACvB,KAAK,8BAA8B,EACnC,eAAe,EACf,KAAK,sBAAsB,EAC3B,gBAAgB,EAChB,KAAK,uBAAuB,GAC7B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,SAAS,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAC5E,OAAO,EAAE,cAAc,EAAE,KAAK,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAE7E,MAAM,WAAW,iBAAkB,SAAQ,eAAe;IACxD,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,gDAAgD;IAChD,GAAG,CAAC,EAAE,IAAI,CAAC,qBAAqB,EAAE,WAAW,CAAC,CAAC;CAChD;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,cAAc,CAAC;IACpB,IAAI,EAAE,gBAAgB,CAAC;IACvB,IAAI,EAAE,gBAAgB,CAAC;IACvB,UAAU,EAAE,uBAAuB,CAAC;IACpC,UAAU,EAAE,eAAe,CAAC;IAC5B,8CAA8C;IAC9C,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED;;;;;;;;;GASG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,KAAK,EACZ,OAAO,GAAE,iBAAsB,GAC9B,OAAO,CAAC,UAAU,CAAC,CAerB"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { RedisSessionLock, RedisSubagentAwaitLease, RedisTreeBudget, RedisWakeChannel, } from './coordination.js';
|
|
2
|
+
import { RedisStreamBus } from './stream-bus.js';
|
|
3
|
+
export { RedisSessionLock, RedisSubagentAwaitLease, RedisTreeBudget, RedisWakeChannel, } from './coordination.js';
|
|
4
|
+
export { redisKeys } from './keys.js';
|
|
5
|
+
export { RedisStreamBus } from './stream-bus.js';
|
|
6
|
+
/**
|
|
7
|
+
* 一组 Redis 实现,键名与 `createInProcessPorts()` 的对应项相同,宿主可以直接覆盖:
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* const ports = { ...createInProcessPorts(), ...(await createRedisPorts(redis)) };
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* `redis` 是宿主的共享连接(锁、租约、名额、写会话流都用它);唤醒频道与每个会话订阅
|
|
14
|
+
* 各自 `duplicate()` 一条。等唤醒频道订阅就绪才返回。
|
|
15
|
+
*/
|
|
16
|
+
export async function createRedisPorts(redis, options = {}) {
|
|
17
|
+
const { keyPrefix, logger } = options;
|
|
18
|
+
const wake = await RedisWakeChannel.create(redis, { keyPrefix, logger });
|
|
19
|
+
const bus = new RedisStreamBus(redis, { ...options.bus, keyPrefix });
|
|
20
|
+
return {
|
|
21
|
+
bus,
|
|
22
|
+
lock: new RedisSessionLock(redis, { keyPrefix }),
|
|
23
|
+
wake,
|
|
24
|
+
awaitLease: new RedisSubagentAwaitLease(redis, { keyPrefix, logger }),
|
|
25
|
+
treeBudget: new RedisTreeBudget(redis, { keyPrefix }),
|
|
26
|
+
async dispose() {
|
|
27
|
+
bus.dispose();
|
|
28
|
+
await wake.dispose();
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAUA,OAAO,EACL,gBAAgB,EAChB,uBAAuB,EACvB,eAAe,EACf,gBAAgB,GACjB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAE,cAAc,EAA8B,MAAM,iBAAiB,CAAC;AAE7E,OAAO,EACL,gBAAgB,EAEhB,uBAAuB,EAEvB,eAAe,EAEf,gBAAgB,GAEjB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAwC,SAAS,EAAE,MAAM,WAAW,CAAC;AAC5E,OAAO,EAAE,cAAc,EAA8B,MAAM,iBAAiB,CAAC;AAkB7E;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,KAAY,EACZ,UAA6B,EAAE;IAE/B,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC;IACtC,MAAM,IAAI,GAAG,MAAM,gBAAgB,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,CAAC;IACzE,MAAM,GAAG,GAAG,IAAI,cAAc,CAAC,KAAK,EAAE,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,SAAS,EAAE,CAAC,CAAC;IACrE,OAAO;QACL,GAAG;QACH,IAAI,EAAE,IAAI,gBAAgB,CAAC,KAAK,EAAE,EAAE,SAAS,EAAE,CAAC;QAChD,IAAI;QACJ,UAAU,EAAE,IAAI,uBAAuB,CAAC,KAAK,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC;QACrE,UAAU,EAAE,IAAI,eAAe,CAAC,KAAK,EAAE,EAAE,SAAS,EAAE,CAAC;QACrD,KAAK,CAAC,OAAO;YACX,GAAG,CAAC,OAAO,EAAE,CAAC;YACd,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;QACvB,CAAC;KACF,CAAC;AACJ,CAAC"}
|
package/dist/keys.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 键与频道名。缺省与 agent-base 线上用的一字不差——agent-base 换上本包时可以滚动发布
|
|
3
|
+
* (新旧实例共用一个 Redis 期间锁、唤醒、租约、会话流都互通),空闲广播的载荷也照旧是
|
|
4
|
+
* JSON `{ sessionId }`,它现有的订阅方不用改。
|
|
5
|
+
*
|
|
6
|
+
* 多个 APP 共用一个 Redis 时给 `keyPrefix`(例如 `'novel:'`),键和 pub/sub 频道都加前缀。
|
|
7
|
+
* 不用 ioredis 自带的 `keyPrefix`:它只管键、不管频道,而且是整条连接的设置,宿主的其它用途会被连带。
|
|
8
|
+
*/
|
|
9
|
+
export interface RedisKeyOptions {
|
|
10
|
+
keyPrefix?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface RedisKeys {
|
|
13
|
+
lock(sessionId: string): string;
|
|
14
|
+
wakeChannel: string;
|
|
15
|
+
idleChannel: string;
|
|
16
|
+
awaited(childSessionId: string): string;
|
|
17
|
+
liveSubs(rootSessionId: string): string;
|
|
18
|
+
sessionStream(rootSessionId: string): string;
|
|
19
|
+
}
|
|
20
|
+
export declare function redisKeys(options?: RedisKeyOptions): RedisKeys;
|
|
21
|
+
//# sourceMappingURL=keys.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,CAAC;IACxC,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAAC;IACxC,aAAa,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAAC;CAC9C;AAED,wBAAgB,SAAS,CAAC,OAAO,GAAE,eAAoB,GAAG,SAAS,CAUlE"}
|
package/dist/keys.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function redisKeys(options = {}) {
|
|
2
|
+
const p = options.keyPrefix ?? '';
|
|
3
|
+
return {
|
|
4
|
+
lock: s => `${p}agent-loop:lock:${s}`,
|
|
5
|
+
wakeChannel: `${p}agent-loop:wake`,
|
|
6
|
+
idleChannel: `${p}agent-session:idle`,
|
|
7
|
+
awaited: c => `${p}agent-subagent:awaited:${c}`,
|
|
8
|
+
liveSubs: r => `${p}agent-tree:live-subs:${r}`,
|
|
9
|
+
sessionStream: r => `${p}agent-session:${r}`,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=keys.js.map
|
package/dist/keys.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keys.js","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAqBA,MAAM,UAAU,SAAS,CAAC,UAA2B,EAAE;IACrD,MAAM,CAAC,GAAG,OAAO,CAAC,SAAS,IAAI,EAAE,CAAC;IAClC,OAAO;QACL,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,mBAAmB,CAAC,EAAE;QACrC,WAAW,EAAE,GAAG,CAAC,iBAAiB;QAClC,WAAW,EAAE,GAAG,CAAC,oBAAoB;QACrC,OAAO,EAAE,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,0BAA0B,CAAC,EAAE;QAC/C,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,wBAAwB,CAAC,EAAE;QAC9C,aAAa,EAAE,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,iBAAiB,CAAC,EAAE;KAC7C,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { Redis } from 'ioredis';
|
|
2
|
+
import { type AgentEvent, InProcessStreamBus, type InProcessStreamBusOptions, type SessionStreamEntry } from '@qjp88995/agent-runtime';
|
|
3
|
+
import { type RedisKeyOptions } from './keys.js';
|
|
4
|
+
export interface RedisStreamBusOptions extends RedisKeyOptions, InProcessStreamBusOptions {
|
|
5
|
+
/** 写侧合并窗口;`null` 关掉合并(一条事件一条条目)。缺省 50ms。 */
|
|
6
|
+
batchMs?: number | null;
|
|
7
|
+
/** 落库后的退役宽限期。缺省 30s。 */
|
|
8
|
+
retireGraceMs?: number;
|
|
9
|
+
/** 会话流的 MAXLEN 兜底。缺省 10000。 */
|
|
10
|
+
maxLen?: number;
|
|
11
|
+
/** 会话流的 TTL(每写一条刷新)。缺省 1 小时;轮次登记表在进程里也活这么久。 */
|
|
12
|
+
streamTtlSeconds?: number;
|
|
13
|
+
/** 一次 XREAD 阻塞多久后回来检查 abort。缺省 5s。 */
|
|
14
|
+
xreadBlockMs?: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 多实例事件总线:**per-stream 面留在进程内**(继承 `InProcessStreamBus`:seq、buffer、取消信号——
|
|
18
|
+
* 消费它们的 pump 与 `streamText` 都在跑这一轮的那个进程里),**会话面换成 Redis Streams**
|
|
19
|
+
* (键 `agent-session:<root>`),跨实例、断线可按游标续订。搬自 agent-base `RedisStreamBus`。
|
|
20
|
+
*
|
|
21
|
+
* ## 会话面只保留尚未落库的尾巴
|
|
22
|
+
*
|
|
23
|
+
* - **落库即退役**:宿主落库成功后调 `retireUpTo(runId, seq)`,宽限期后 XDEL 这一轮 seq ≤ 边界的条目;
|
|
24
|
+
* `run-finish` / `error` 宽限期后整轮清掉。条目 id 从 XADD 回值登记在进程内存里(写这轮事件的
|
|
25
|
+
* 就是跑这轮的进程);别的实例补发的 `run-finish` 在本进程查不到表,删除是 no-op,孤儿条目由
|
|
26
|
+
* TTL / MAXLEN 兜底。
|
|
27
|
+
* - **写侧合并**:连续的同段 delta 攒一个窗口再写;任何不可合并的事件先把攒着的吐掉,顺序不变。
|
|
28
|
+
*
|
|
29
|
+
* 会话面的写是 fire-and-forget:观测不承重,序列化失败或 Redis 出错只丢这一条,绝不让一轮对话失败。
|
|
30
|
+
*
|
|
31
|
+
* ## 与 agent-base 的差别
|
|
32
|
+
*
|
|
33
|
+
* - `publish` 只在流活着时写会话面(agent-base 不看流状态;这里与进程内实现、端口约定对齐:
|
|
34
|
+
* 「流未 open / 已 close 时丢弃」)。
|
|
35
|
+
* - 测试用的 `configureForTest` 改成构造参数 `batchMs` / `retireGraceMs`;加 `dispose()`。
|
|
36
|
+
* - 键可加前缀(`keyPrefix`)。
|
|
37
|
+
*
|
|
38
|
+
* **停止按钮仍是进程内的**:`abort(streamId)` 只对本进程跑着的流生效,与 agent-base 一样——
|
|
39
|
+
* 停止请求落到别的实例上时,宿主要自己把它路由到跑这轮的实例(或者走会话锁所在实例)。
|
|
40
|
+
*/
|
|
41
|
+
export declare class RedisStreamBus extends InProcessStreamBus {
|
|
42
|
+
private readonly redis;
|
|
43
|
+
private readonly keys;
|
|
44
|
+
private readonly batchMs;
|
|
45
|
+
private readonly graceMs;
|
|
46
|
+
private readonly maxLen;
|
|
47
|
+
private readonly streamTtlSeconds;
|
|
48
|
+
private readonly xreadBlockMs;
|
|
49
|
+
private readonly pendingByRoot;
|
|
50
|
+
private readonly entriesByRun;
|
|
51
|
+
private readonly timers;
|
|
52
|
+
private readonly runTableTimer;
|
|
53
|
+
constructor(redis: Redis, options?: RedisStreamBusOptions);
|
|
54
|
+
publish(event: AgentEvent): void;
|
|
55
|
+
publishSessionEvent(event: AgentEvent): void;
|
|
56
|
+
retireUpTo(runId: string, uptoSeq: number): void;
|
|
57
|
+
/** 宽限期后执行;`dispose()` 会清掉还没到点的。 */
|
|
58
|
+
private later;
|
|
59
|
+
/**
|
|
60
|
+
* 会话面入口。序列化在进入 fire-and-forget 链之前同步做完(见 `xaddSession`),非 JSON-safe
|
|
61
|
+
* 载荷不会经 `publish` 冒进 `runTurn`。
|
|
62
|
+
*/
|
|
63
|
+
private enqueueSession;
|
|
64
|
+
private flushPending;
|
|
65
|
+
/** 写一条并登记;返回的 promise 在登记完(或失败被吞)后落定,从不 reject。 */
|
|
66
|
+
private xaddSession;
|
|
67
|
+
private deleteEntries;
|
|
68
|
+
/** 轮次登记表兜底清理:正常路径由 run-finish / error 清;崩溃或别的实例收尾的轮次靠这里。 */
|
|
69
|
+
private sweepRunTables;
|
|
70
|
+
/**
|
|
71
|
+
* 每个订阅一条独立连接(XREAD BLOCK 会占住连接);`signal` abort 时断开它,在途的 XREAD 随之结束。
|
|
72
|
+
*/
|
|
73
|
+
subscribeSession(rootSessionId: string, fromId: string | undefined, signal: AbortSignal): AsyncIterable<SessionStreamEntry>;
|
|
74
|
+
/**
|
|
75
|
+
* 停掉定时器:攒着的 delta 立刻写出去(不丢),还没到点的退役放弃(条目由 TTL / MAXLEN 兜底)。
|
|
76
|
+
* 不关宿主给的连接。
|
|
77
|
+
*/
|
|
78
|
+
dispose(): void;
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=stream-bus.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stream-bus.d.ts","sourceRoot":"","sources":["../src/stream-bus.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAErC,OAAO,EACL,KAAK,UAAU,EACf,kBAAkB,EAClB,KAAK,yBAAyB,EAC9B,KAAK,kBAAkB,EACxB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EAAE,KAAK,eAAe,EAA6B,MAAM,WAAW,CAAC;AA+E5E,MAAM,WAAW,qBACf,SAAQ,eAAe,EAAE,yBAAyB;IAClD,4CAA4C;IAC5C,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,wBAAwB;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,+BAA+B;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,+CAA+C;IAC/C,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,sCAAsC;IACtC,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,qBAAa,cAAe,SAAQ,kBAAkB;IAalD,OAAO,CAAC,QAAQ,CAAC,KAAK;IAZxB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAY;IACjC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgB;IACxC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAC1C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAmC;IACjE,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAiC;IAC9D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA6B;IACpD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAiB;gBAG5B,KAAK,EAAE,KAAK,EAC7B,OAAO,GAAE,qBAA0B;IAiB5B,OAAO,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;IAKhC,mBAAmB,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;IAI5C,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAIzD,mCAAmC;IACnC,OAAO,CAAC,KAAK;IASb;;;OAGG;IACH,OAAO,CAAC,cAAc;IAiCtB,OAAO,CAAC,YAAY;IAQpB,mDAAmD;IACnD,OAAO,CAAC,WAAW;IA4BnB,OAAO,CAAC,aAAa;IAiBrB,4DAA4D;IAC5D,OAAO,CAAC,cAAc;IAQtB;;OAEG;IACa,gBAAgB,CAC9B,aAAa,EAAE,MAAM,EACrB,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,MAAM,EAAE,WAAW,GAClB,aAAa,CAAC,kBAAkB,CAAC;IA2CpC;;;OAGG;IACM,OAAO,IAAI,IAAI;CAOzB"}
|