@godman-protocols/signal 0.2.0 → 0.3.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/index.d.ts +5 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -6
- package/dist/index.js.map +1 -1
- package/dist/mode-controller.d.ts +71 -0
- package/dist/mode-controller.d.ts.map +1 -0
- package/dist/mode-controller.js +112 -0
- package/dist/mode-controller.js.map +1 -0
- package/dist/multiplier.d.ts +22 -0
- package/dist/multiplier.d.ts.map +1 -0
- package/dist/multiplier.js +78 -0
- package/dist/multiplier.js.map +1 -0
- package/dist/reward-logger.d.ts +34 -0
- package/dist/reward-logger.d.ts.map +1 -0
- package/dist/reward-logger.js +62 -0
- package/dist/reward-logger.js.map +1 -0
- package/dist/types.d.ts +122 -47
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +65 -4
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +5 -24
- package/src/mode-controller.ts +135 -0
- package/src/multiplier.ts +95 -0
- package/src/reward-logger.ts +73 -0
- package/src/types.ts +167 -51
- package/dist/bus.d.ts +0 -53
- package/dist/bus.d.ts.map +0 -1
- package/dist/bus.js +0 -141
- package/dist/bus.js.map +0 -1
- package/src/bus.ts +0 -180
package/dist/bus.d.ts
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SIGNAL — Event Bus and Pub/Sub for Agent Swarms
|
|
3
|
-
* In-memory EventBus implementation
|
|
4
|
-
* @version 0.2.0
|
|
5
|
-
*/
|
|
6
|
-
import type { AgentId, DeliveryReceipt, Event, Subscription, SubscriptionId, Timestamp } from './types.js';
|
|
7
|
-
/**
|
|
8
|
-
* Match a topic against a glob-style filter.
|
|
9
|
-
* - `*` matches exactly one segment
|
|
10
|
-
* - `**` matches zero or more segments
|
|
11
|
-
* - Exact strings match literally
|
|
12
|
-
*/
|
|
13
|
-
export declare function topicMatches(filter: string, topic: string): boolean;
|
|
14
|
-
/**
|
|
15
|
-
* Create a signed event.
|
|
16
|
-
*/
|
|
17
|
-
export declare function createEvent(publisher: AgentId, topic: string, payload: unknown, secret: string, options?: {
|
|
18
|
-
id?: string;
|
|
19
|
-
idempotencyKey?: string;
|
|
20
|
-
publishedAt?: Timestamp;
|
|
21
|
-
}): Event;
|
|
22
|
-
type EventHandler = (event: Event) => void | Promise<void>;
|
|
23
|
-
export declare class EventBus {
|
|
24
|
-
private subs;
|
|
25
|
-
private seenIdempotencyKeys;
|
|
26
|
-
private receipts;
|
|
27
|
-
/**
|
|
28
|
-
* Subscribe to events matching a topic filter.
|
|
29
|
-
*/
|
|
30
|
-
subscribe(subscriberAgent: AgentId, topicFilter: string, handler: EventHandler, deliveryMode?: 'at-least-once' | 'at-most-once'): Subscription;
|
|
31
|
-
/**
|
|
32
|
-
* Unsubscribe. Marks the subscription as cancelled.
|
|
33
|
-
*/
|
|
34
|
-
unsubscribe(subscriptionId: SubscriptionId): void;
|
|
35
|
-
/**
|
|
36
|
-
* Publish an event to all matching subscribers.
|
|
37
|
-
* Deduplicates by idempotencyKey.
|
|
38
|
-
* Returns delivery receipts.
|
|
39
|
-
*/
|
|
40
|
-
publish(event: Event): Promise<DeliveryReceipt[]>;
|
|
41
|
-
/**
|
|
42
|
-
* Get all delivery receipts.
|
|
43
|
-
*/
|
|
44
|
-
getReceipts(): ReadonlyArray<DeliveryReceipt>;
|
|
45
|
-
/**
|
|
46
|
-
* Get active subscription count.
|
|
47
|
-
*/
|
|
48
|
-
get subscriptionCount(): number;
|
|
49
|
-
}
|
|
50
|
-
/** Default singleton bus for single-process use */
|
|
51
|
-
export declare const defaultBus: EventBus;
|
|
52
|
-
export {};
|
|
53
|
-
//# sourceMappingURL=bus.d.ts.map
|
package/dist/bus.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"bus.d.ts","sourceRoot":"","sources":["../src/bus.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EACV,OAAO,EACP,eAAe,EACf,KAAK,EACL,YAAY,EACZ,cAAc,EACd,SAAS,EACV,MAAM,YAAY,CAAC;AAMpB;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAInE;AAuBD;;GAEG;AACH,wBAAgB,WAAW,CACzB,SAAS,EAAE,OAAO,EAClB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,MAAM,EACd,OAAO,GAAE;IAAE,EAAE,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,SAAS,CAAA;CAAO,GAC9E,KAAK,CAOP;AAMD,KAAK,YAAY,GAAG,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAO3D,qBAAa,QAAQ;IACnB,OAAO,CAAC,IAAI,CAA0C;IACtD,OAAO,CAAC,mBAAmB,CAAqB;IAChD,OAAO,CAAC,QAAQ,CAAyB;IAEzC;;OAEG;IACH,SAAS,CACP,eAAe,EAAE,OAAO,EACxB,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,YAAY,EACrB,YAAY,GAAE,eAAe,GAAG,cAAgC,GAC/D,YAAY;IAaf;;OAEG;IACH,WAAW,CAAC,cAAc,EAAE,cAAc,GAAG,IAAI;IAOjD;;;;OAIG;IACG,OAAO,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IAwCvD;;OAEG;IACH,WAAW,IAAI,aAAa,CAAC,eAAe,CAAC;IAI7C;;OAEG;IACH,IAAI,iBAAiB,IAAI,MAAM,CAE9B;CACF;AAED,mDAAmD;AACnD,eAAO,MAAM,UAAU,UAAiB,CAAC"}
|
package/dist/bus.js
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SIGNAL — Event Bus and Pub/Sub for Agent Swarms
|
|
3
|
-
* In-memory EventBus implementation
|
|
4
|
-
* @version 0.2.0
|
|
5
|
-
*/
|
|
6
|
-
import { createHmac, randomUUID } from 'node:crypto';
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
// Topic matching
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
/**
|
|
11
|
-
* Match a topic against a glob-style filter.
|
|
12
|
-
* - `*` matches exactly one segment
|
|
13
|
-
* - `**` matches zero or more segments
|
|
14
|
-
* - Exact strings match literally
|
|
15
|
-
*/
|
|
16
|
-
export function topicMatches(filter, topic) {
|
|
17
|
-
const filterParts = filter.split('.');
|
|
18
|
-
const topicParts = topic.split('.');
|
|
19
|
-
return matchParts(filterParts, 0, topicParts, 0);
|
|
20
|
-
}
|
|
21
|
-
function matchParts(f, fi, t, ti) {
|
|
22
|
-
if (fi === f.length && ti === t.length)
|
|
23
|
-
return true;
|
|
24
|
-
if (fi === f.length)
|
|
25
|
-
return false;
|
|
26
|
-
if (f[fi] === '**') {
|
|
27
|
-
// ** can match zero or more segments
|
|
28
|
-
for (let i = ti; i <= t.length; i++) {
|
|
29
|
-
if (matchParts(f, fi + 1, t, i))
|
|
30
|
-
return true;
|
|
31
|
-
}
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
if (ti === t.length)
|
|
35
|
-
return false;
|
|
36
|
-
if (f[fi] === '*' || f[fi] === t[ti]) {
|
|
37
|
-
return matchParts(f, fi + 1, t, ti + 1);
|
|
38
|
-
}
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
|
-
// ---------------------------------------------------------------------------
|
|
42
|
-
// Event creation
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
/**
|
|
45
|
-
* Create a signed event.
|
|
46
|
-
*/
|
|
47
|
-
export function createEvent(publisher, topic, payload, secret, options = {}) {
|
|
48
|
-
const id = options.id ?? randomUUID();
|
|
49
|
-
const publishedAt = options.publishedAt ?? new Date().toISOString();
|
|
50
|
-
const idempotencyKey = options.idempotencyKey ?? id;
|
|
51
|
-
const sigPayload = `${id}:${topic}:${publisher}:${publishedAt}`;
|
|
52
|
-
const signature = createHmac('sha256', secret).update(sigPayload, 'utf8').digest('hex');
|
|
53
|
-
return { id, topic, publisher, publishedAt, idempotencyKey, payload, signature };
|
|
54
|
-
}
|
|
55
|
-
export class EventBus {
|
|
56
|
-
subs = new Map();
|
|
57
|
-
seenIdempotencyKeys = new Set();
|
|
58
|
-
receipts = [];
|
|
59
|
-
/**
|
|
60
|
-
* Subscribe to events matching a topic filter.
|
|
61
|
-
*/
|
|
62
|
-
subscribe(subscriberAgent, topicFilter, handler, deliveryMode = 'at-least-once') {
|
|
63
|
-
const sub = {
|
|
64
|
-
id: randomUUID(),
|
|
65
|
-
subscriberAgent,
|
|
66
|
-
topicFilter,
|
|
67
|
-
deliveryMode,
|
|
68
|
-
createdAt: new Date().toISOString(),
|
|
69
|
-
cancelledAt: null,
|
|
70
|
-
};
|
|
71
|
-
this.subs.set(sub.id, { subscription: sub, handler });
|
|
72
|
-
return sub;
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Unsubscribe. Marks the subscription as cancelled.
|
|
76
|
-
*/
|
|
77
|
-
unsubscribe(subscriptionId) {
|
|
78
|
-
const entry = this.subs.get(subscriptionId);
|
|
79
|
-
if (!entry)
|
|
80
|
-
throw new Error(`Subscription ${subscriptionId} not found`);
|
|
81
|
-
entry.subscription.cancelledAt = new Date().toISOString();
|
|
82
|
-
this.subs.delete(subscriptionId);
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Publish an event to all matching subscribers.
|
|
86
|
-
* Deduplicates by idempotencyKey.
|
|
87
|
-
* Returns delivery receipts.
|
|
88
|
-
*/
|
|
89
|
-
async publish(event) {
|
|
90
|
-
// Idempotency check
|
|
91
|
-
if (this.seenIdempotencyKeys.has(event.idempotencyKey)) {
|
|
92
|
-
const receipt = {
|
|
93
|
-
eventId: event.id,
|
|
94
|
-
subscriptionId: 'none',
|
|
95
|
-
receivedAt: new Date().toISOString(),
|
|
96
|
-
status: 'duplicate-skipped',
|
|
97
|
-
};
|
|
98
|
-
this.receipts.push(receipt);
|
|
99
|
-
return [receipt];
|
|
100
|
-
}
|
|
101
|
-
this.seenIdempotencyKeys.add(event.idempotencyKey);
|
|
102
|
-
const newReceipts = [];
|
|
103
|
-
for (const [, entry] of this.subs) {
|
|
104
|
-
if (entry.subscription.cancelledAt)
|
|
105
|
-
continue;
|
|
106
|
-
if (!topicMatches(entry.subscription.topicFilter, event.topic))
|
|
107
|
-
continue;
|
|
108
|
-
let status = 'processed';
|
|
109
|
-
try {
|
|
110
|
-
await entry.handler(event);
|
|
111
|
-
}
|
|
112
|
-
catch {
|
|
113
|
-
status = 'failed';
|
|
114
|
-
}
|
|
115
|
-
const receipt = {
|
|
116
|
-
eventId: event.id,
|
|
117
|
-
subscriptionId: entry.subscription.id,
|
|
118
|
-
receivedAt: new Date().toISOString(),
|
|
119
|
-
status,
|
|
120
|
-
};
|
|
121
|
-
newReceipts.push(receipt);
|
|
122
|
-
}
|
|
123
|
-
this.receipts.push(...newReceipts);
|
|
124
|
-
return newReceipts;
|
|
125
|
-
}
|
|
126
|
-
/**
|
|
127
|
-
* Get all delivery receipts.
|
|
128
|
-
*/
|
|
129
|
-
getReceipts() {
|
|
130
|
-
return [...this.receipts];
|
|
131
|
-
}
|
|
132
|
-
/**
|
|
133
|
-
* Get active subscription count.
|
|
134
|
-
*/
|
|
135
|
-
get subscriptionCount() {
|
|
136
|
-
return this.subs.size;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
/** Default singleton bus for single-process use */
|
|
140
|
-
export const defaultBus = new EventBus();
|
|
141
|
-
//# sourceMappingURL=bus.js.map
|
package/dist/bus.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"bus.js","sourceRoot":"","sources":["../src/bus.ts"],"names":[],"mappings":"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"}
|
package/src/bus.ts
DELETED
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SIGNAL — Event Bus and Pub/Sub for Agent Swarms
|
|
3
|
-
* In-memory EventBus implementation
|
|
4
|
-
* @version 0.2.0
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { createHmac, randomUUID } from 'node:crypto';
|
|
8
|
-
import type {
|
|
9
|
-
AgentId,
|
|
10
|
-
DeliveryReceipt,
|
|
11
|
-
Event,
|
|
12
|
-
Subscription,
|
|
13
|
-
SubscriptionId,
|
|
14
|
-
Timestamp,
|
|
15
|
-
} from './types.js';
|
|
16
|
-
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
|
-
// Topic matching
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Match a topic against a glob-style filter.
|
|
23
|
-
* - `*` matches exactly one segment
|
|
24
|
-
* - `**` matches zero or more segments
|
|
25
|
-
* - Exact strings match literally
|
|
26
|
-
*/
|
|
27
|
-
export function topicMatches(filter: string, topic: string): boolean {
|
|
28
|
-
const filterParts = filter.split('.');
|
|
29
|
-
const topicParts = topic.split('.');
|
|
30
|
-
return matchParts(filterParts, 0, topicParts, 0);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function matchParts(f: string[], fi: number, t: string[], ti: number): boolean {
|
|
34
|
-
if (fi === f.length && ti === t.length) return true;
|
|
35
|
-
if (fi === f.length) return false;
|
|
36
|
-
if (f[fi] === '**') {
|
|
37
|
-
// ** can match zero or more segments
|
|
38
|
-
for (let i = ti; i <= t.length; i++) {
|
|
39
|
-
if (matchParts(f, fi + 1, t, i)) return true;
|
|
40
|
-
}
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
if (ti === t.length) return false;
|
|
44
|
-
if (f[fi] === '*' || f[fi] === t[ti]) {
|
|
45
|
-
return matchParts(f, fi + 1, t, ti + 1);
|
|
46
|
-
}
|
|
47
|
-
return false;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// ---------------------------------------------------------------------------
|
|
51
|
-
// Event creation
|
|
52
|
-
// ---------------------------------------------------------------------------
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Create a signed event.
|
|
56
|
-
*/
|
|
57
|
-
export function createEvent(
|
|
58
|
-
publisher: AgentId,
|
|
59
|
-
topic: string,
|
|
60
|
-
payload: unknown,
|
|
61
|
-
secret: string,
|
|
62
|
-
options: { id?: string; idempotencyKey?: string; publishedAt?: Timestamp } = {}
|
|
63
|
-
): Event {
|
|
64
|
-
const id = options.id ?? randomUUID();
|
|
65
|
-
const publishedAt = options.publishedAt ?? new Date().toISOString();
|
|
66
|
-
const idempotencyKey = options.idempotencyKey ?? id;
|
|
67
|
-
const sigPayload = `${id}:${topic}:${publisher}:${publishedAt}`;
|
|
68
|
-
const signature = createHmac('sha256', secret).update(sigPayload, 'utf8').digest('hex');
|
|
69
|
-
return { id, topic, publisher, publishedAt, idempotencyKey, payload, signature };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// ---------------------------------------------------------------------------
|
|
73
|
-
// EventBus
|
|
74
|
-
// ---------------------------------------------------------------------------
|
|
75
|
-
|
|
76
|
-
type EventHandler = (event: Event) => void | Promise<void>;
|
|
77
|
-
|
|
78
|
-
interface InternalSub {
|
|
79
|
-
subscription: Subscription;
|
|
80
|
-
handler: EventHandler;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export class EventBus {
|
|
84
|
-
private subs = new Map<SubscriptionId, InternalSub>();
|
|
85
|
-
private seenIdempotencyKeys = new Set<string>();
|
|
86
|
-
private receipts: DeliveryReceipt[] = [];
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Subscribe to events matching a topic filter.
|
|
90
|
-
*/
|
|
91
|
-
subscribe(
|
|
92
|
-
subscriberAgent: AgentId,
|
|
93
|
-
topicFilter: string,
|
|
94
|
-
handler: EventHandler,
|
|
95
|
-
deliveryMode: 'at-least-once' | 'at-most-once' = 'at-least-once'
|
|
96
|
-
): Subscription {
|
|
97
|
-
const sub: Subscription = {
|
|
98
|
-
id: randomUUID(),
|
|
99
|
-
subscriberAgent,
|
|
100
|
-
topicFilter,
|
|
101
|
-
deliveryMode,
|
|
102
|
-
createdAt: new Date().toISOString(),
|
|
103
|
-
cancelledAt: null,
|
|
104
|
-
};
|
|
105
|
-
this.subs.set(sub.id, { subscription: sub, handler });
|
|
106
|
-
return sub;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Unsubscribe. Marks the subscription as cancelled.
|
|
111
|
-
*/
|
|
112
|
-
unsubscribe(subscriptionId: SubscriptionId): void {
|
|
113
|
-
const entry = this.subs.get(subscriptionId);
|
|
114
|
-
if (!entry) throw new Error(`Subscription ${subscriptionId} not found`);
|
|
115
|
-
entry.subscription.cancelledAt = new Date().toISOString();
|
|
116
|
-
this.subs.delete(subscriptionId);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Publish an event to all matching subscribers.
|
|
121
|
-
* Deduplicates by idempotencyKey.
|
|
122
|
-
* Returns delivery receipts.
|
|
123
|
-
*/
|
|
124
|
-
async publish(event: Event): Promise<DeliveryReceipt[]> {
|
|
125
|
-
// Idempotency check
|
|
126
|
-
if (this.seenIdempotencyKeys.has(event.idempotencyKey)) {
|
|
127
|
-
const receipt: DeliveryReceipt = {
|
|
128
|
-
eventId: event.id,
|
|
129
|
-
subscriptionId: 'none',
|
|
130
|
-
receivedAt: new Date().toISOString(),
|
|
131
|
-
status: 'duplicate-skipped',
|
|
132
|
-
};
|
|
133
|
-
this.receipts.push(receipt);
|
|
134
|
-
return [receipt];
|
|
135
|
-
}
|
|
136
|
-
this.seenIdempotencyKeys.add(event.idempotencyKey);
|
|
137
|
-
|
|
138
|
-
const newReceipts: DeliveryReceipt[] = [];
|
|
139
|
-
|
|
140
|
-
for (const [, entry] of this.subs) {
|
|
141
|
-
if (entry.subscription.cancelledAt) continue;
|
|
142
|
-
if (!topicMatches(entry.subscription.topicFilter, event.topic)) continue;
|
|
143
|
-
|
|
144
|
-
let status: DeliveryReceipt['status'] = 'processed';
|
|
145
|
-
try {
|
|
146
|
-
await entry.handler(event);
|
|
147
|
-
} catch {
|
|
148
|
-
status = 'failed';
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const receipt: DeliveryReceipt = {
|
|
152
|
-
eventId: event.id,
|
|
153
|
-
subscriptionId: entry.subscription.id,
|
|
154
|
-
receivedAt: new Date().toISOString(),
|
|
155
|
-
status,
|
|
156
|
-
};
|
|
157
|
-
newReceipts.push(receipt);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
this.receipts.push(...newReceipts);
|
|
161
|
-
return newReceipts;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Get all delivery receipts.
|
|
166
|
-
*/
|
|
167
|
-
getReceipts(): ReadonlyArray<DeliveryReceipt> {
|
|
168
|
-
return [...this.receipts];
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Get active subscription count.
|
|
173
|
-
*/
|
|
174
|
-
get subscriptionCount(): number {
|
|
175
|
-
return this.subs.size;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/** Default singleton bus for single-process use */
|
|
180
|
-
export const defaultBus = new EventBus();
|