@matatbread/matbot-triggers 0.3.2 → 0.3.4
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/package.json +3 -3
- package/src/manager.ts +24 -28
- package/src/plugin.ts +2 -5
- package/src/tools.ts +9 -6
- package/src/types.ts +3 -3
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"node",
|
|
5
5
|
"browser"
|
|
6
6
|
],
|
|
7
|
-
"version": "0.3.
|
|
7
|
+
"version": "0.3.4",
|
|
8
8
|
"engines": {
|
|
9
9
|
"node": ">=24.0.0"
|
|
10
10
|
},
|
|
@@ -33,11 +33,11 @@
|
|
|
33
33
|
"access": "public"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
|
-
"@matatbread/matbot-plugin-api": "^0.3.
|
|
36
|
+
"@matatbread/matbot-plugin-api": "^0.3.4"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"typescript": "^6.0.3",
|
|
40
|
-
"@matatbread/matbot-plugin-api": "^0.3.
|
|
40
|
+
"@matatbread/matbot-plugin-api": "^0.3.4"
|
|
41
41
|
},
|
|
42
42
|
"scripts": {
|
|
43
43
|
"typecheck": "tsc --noEmit"
|
package/src/manager.ts
CHANGED
|
@@ -27,7 +27,6 @@ function invokeKey(t: { invoke: Trigger['invoke'] }): string {
|
|
|
27
27
|
* Constructed only with web-platform primitives, so it runs in the browser too.
|
|
28
28
|
*/
|
|
29
29
|
export class TriggerManager implements Triggers {
|
|
30
|
-
private readonly triggers = new Map<string, Trigger>();
|
|
31
30
|
private readonly store: Store<Trigger>;
|
|
32
31
|
private readonly services: MatbotMachine;
|
|
33
32
|
// Aborts on teardown (clear()), ending the mounted-swap subscription set up in setupTriggers.
|
|
@@ -38,8 +37,7 @@ export class TriggerManager implements Triggers {
|
|
|
38
37
|
this.services = services;
|
|
39
38
|
}
|
|
40
39
|
|
|
41
|
-
/**
|
|
42
|
-
* re-reads the new backend's triggers, and the loop stops when the plugin unloads. */
|
|
40
|
+
/** Aborts on teardown (clear()) — ends any in-flight classifier call the manager owns. */
|
|
43
41
|
get signal(): AbortSignal { return this.lifecycle.signal; }
|
|
44
42
|
|
|
45
43
|
// The classifier provider, resolved live per evaluation (so a triggers_config change takes effect on
|
|
@@ -53,26 +51,25 @@ export class TriggerManager implements Triggers {
|
|
|
53
51
|
return turnProvider;
|
|
54
52
|
}
|
|
55
53
|
|
|
56
|
-
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
|
|
61
|
-
|
|
54
|
+
/** Read straight through the backing store on every call — no in-memory snapshot. The store is a
|
|
55
|
+
* swap-following proxy, so this always reflects the live backend AND the current principal's
|
|
56
|
+
* partition, and a shared backend's out-of-band writes are seen too. A snapshot here would be
|
|
57
|
+
* principal-blind and go stale under any second writer; caching, if a slow backend needs it,
|
|
58
|
+
* belongs in the StorageBackend, not in this consumer. */
|
|
59
|
+
async all(): Promise<Trigger[]> {
|
|
62
60
|
const { items } = await this.store.query({});
|
|
63
|
-
|
|
64
|
-
this.triggers.set(t.id, t);
|
|
65
|
-
}
|
|
61
|
+
return items;
|
|
66
62
|
}
|
|
67
63
|
|
|
68
|
-
|
|
69
|
-
|
|
64
|
+
async get(id: string): Promise<Trigger | undefined> {
|
|
65
|
+
return (await this.store.get(id)) ?? undefined;
|
|
66
|
+
}
|
|
70
67
|
|
|
71
68
|
/** Triggers whose invocation matches the filter: `tool` (if given) must equal `invoke.tool`, and
|
|
72
69
|
* `params` (if given) must deep-equal `invoke.params`. The natural "which trigger(s) fire tool X
|
|
73
70
|
* (with these args)" lookup — e.g. the one that loads a given skill. */
|
|
74
|
-
query(filter: { tool?: string; params?: unknown }): Trigger[] {
|
|
75
|
-
return this.all().filter(t => {
|
|
71
|
+
async query(filter: { tool?: string; params?: unknown }): Promise<Trigger[]> {
|
|
72
|
+
return (await this.all()).filter(t => {
|
|
76
73
|
if (filter.tool !== undefined && t.invoke.tool !== filter.tool) return false;
|
|
77
74
|
if (filter.params !== undefined &&
|
|
78
75
|
JSON.stringify(t.invoke.params ?? null) !== JSON.stringify(filter.params)) return false;
|
|
@@ -92,13 +89,12 @@ export class TriggerManager implements Triggers {
|
|
|
92
89
|
updatedAt: now,
|
|
93
90
|
};
|
|
94
91
|
await this.store.set(doc.id, doc);
|
|
95
|
-
this.triggers.set(doc.id, doc);
|
|
96
92
|
return doc;
|
|
97
93
|
}
|
|
98
94
|
|
|
99
95
|
async update(id: string, patch: Partial<TriggerSpec>): Promise<Trigger | undefined> {
|
|
100
|
-
const cur = this.
|
|
101
|
-
if (cur ===
|
|
96
|
+
const cur = await this.store.get(id);
|
|
97
|
+
if (cur === null) return undefined;
|
|
102
98
|
return this.casMutate(cur, prev => ({
|
|
103
99
|
...prev,
|
|
104
100
|
...(patch.conditions !== undefined ? { conditions: patch.conditions } : {}),
|
|
@@ -110,21 +106,20 @@ export class TriggerManager implements Triggers {
|
|
|
110
106
|
}
|
|
111
107
|
|
|
112
108
|
async remove(id: string): Promise<boolean> {
|
|
113
|
-
const cur = this.
|
|
114
|
-
if (cur ===
|
|
109
|
+
const cur = await this.store.get(id);
|
|
110
|
+
if (cur === null) return false;
|
|
115
111
|
await this.store.delete(id, cur.version);
|
|
116
|
-
this.triggers.delete(id);
|
|
117
112
|
return true;
|
|
118
113
|
}
|
|
119
114
|
|
|
120
115
|
async importIfAbsent(spec: TriggerSpec): Promise<Trigger> {
|
|
121
116
|
const key = invokeKey(spec);
|
|
122
|
-
const existing = this.all().find(t => invokeKey(t) === key);
|
|
117
|
+
const existing = (await this.all()).find(t => invokeKey(t) === key);
|
|
123
118
|
if (existing !== undefined) return existing;
|
|
124
119
|
return this.add(spec);
|
|
125
120
|
}
|
|
126
121
|
|
|
127
|
-
clear(): void { this.lifecycle.abort();
|
|
122
|
+
clear(): void { this.lifecycle.abort(); }
|
|
128
123
|
|
|
129
124
|
/**
|
|
130
125
|
* LLM-judge every enabled condition on `surface` against the current turn and return the distinct
|
|
@@ -148,7 +143,8 @@ export class TriggerManager implements Triggers {
|
|
|
148
143
|
// Candidate key is `${triggerId}#${conditionIndex}` — addressing conditions by index is fine
|
|
149
144
|
// because evaluation is per-turn and the trigger set is stable for its duration. The surface a
|
|
150
145
|
// condition belongs to is derived from its `kind` (ephemeral/contextual→user, retract/followup→agent).
|
|
151
|
-
const
|
|
146
|
+
const triggers = await this.all();
|
|
147
|
+
const candidates = triggers
|
|
152
148
|
.filter(t => t.enabled !== false)
|
|
153
149
|
.flatMap(t => t.conditions
|
|
154
150
|
.map((c, i) => ({ triggerId: t.id, key: `${t.id}#${i}`, index: i, kind: c.kind, rule: c.rule }))
|
|
@@ -195,8 +191,9 @@ export class TriggerManager implements Triggers {
|
|
|
195
191
|
list.push({ index: c.index, kind: c.kind, rule: c.rule, ...(why !== undefined ? { why } : {}) });
|
|
196
192
|
firedByTrigger.set(c.triggerId, list);
|
|
197
193
|
}
|
|
194
|
+
const byId = new Map(triggers.map(t => [t.id, t]));
|
|
198
195
|
return [...firedByTrigger].flatMap(([id, matched]) => {
|
|
199
|
-
const trigger =
|
|
196
|
+
const trigger = byId.get(id);
|
|
200
197
|
return trigger ? [{ trigger, kinds: [...new Set(matched.map(m => m.kind))], matched }] : [];
|
|
201
198
|
});
|
|
202
199
|
}
|
|
@@ -206,11 +203,10 @@ export class TriggerManager implements Triggers {
|
|
|
206
203
|
for (;;) {
|
|
207
204
|
const next = mutate(cur);
|
|
208
205
|
const r = await this.store.cas(cur.id, cur.version, next);
|
|
209
|
-
if (r.ok)
|
|
206
|
+
if (r.ok) return next;
|
|
210
207
|
const fresh = await this.store.get(cur.id);
|
|
211
208
|
if (fresh === null) {
|
|
212
209
|
await this.store.set(next.id, next);
|
|
213
|
-
this.triggers.set(next.id, next);
|
|
214
210
|
return next;
|
|
215
211
|
}
|
|
216
212
|
cur = fresh;
|
package/src/plugin.ts
CHANGED
|
@@ -148,11 +148,8 @@ export async function setupTriggers(services: MatbotMachine): Promise<TriggerMan
|
|
|
148
148
|
|
|
149
149
|
const store = services.createStore<Trigger>('triggers') as Store<Trigger>;
|
|
150
150
|
const manager = new TriggerManager(store, services);
|
|
151
|
-
|
|
152
|
-
//
|
|
153
|
-
// the old in-memory set. No `replay` — the initial load is the boot load above; this reacts only to
|
|
154
|
-
// future swaps. Ends with the manager (teardown aborts manager.signal).
|
|
155
|
-
services.mounted.consume({ key: 'StorageBackend', signal: manager.signal }, () => void manager.load());
|
|
151
|
+
// No boot load and no StorageBackend-swap re-read: the manager reads straight through `store` (a
|
|
152
|
+
// swap-following proxy) on every call, so it always reflects the live backend and current partition.
|
|
156
153
|
await services.register('Triggers', manager);
|
|
157
154
|
|
|
158
155
|
services.tools.register(createTriggerActionTool(manager));
|
package/src/tools.ts
CHANGED
|
@@ -90,20 +90,20 @@ export function createTriggerActionTool(manager: TriggerManager): Tool<ToolResul
|
|
|
90
90
|
|
|
91
91
|
switch (args.action) {
|
|
92
92
|
case 'list': {
|
|
93
|
-
yield { type: 'result', value: { triggers: manager.all() } };
|
|
93
|
+
yield { type: 'result', value: { triggers: await manager.all() } };
|
|
94
94
|
return;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
case 'query': {
|
|
98
98
|
const a = args as Extract<TriggerActionInput, { action: 'query' }>;
|
|
99
|
-
yield { type: 'result', value: { triggers: manager.query(queryFilter(a)) } };
|
|
99
|
+
yield { type: 'result', value: { triggers: await manager.query(queryFilter(a)) } };
|
|
100
100
|
return;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
case 'get': {
|
|
104
104
|
const { id } = args as Extract<TriggerActionInput, { action: 'get' }>;
|
|
105
105
|
if (!id) { yield { type: 'error', message: 'action "get" requires "id".' }; return; }
|
|
106
|
-
const t = manager.get(id);
|
|
106
|
+
const t = await manager.get(id);
|
|
107
107
|
if (!t) { yield { type: 'error', message: `Trigger not found: "${id}"` }; return; }
|
|
108
108
|
yield { type: 'result', value: t };
|
|
109
109
|
return;
|
|
@@ -128,10 +128,13 @@ export function createTriggerActionTool(manager: TriggerManager): Tool<ToolResul
|
|
|
128
128
|
if (!a.id) { yield { type: 'error', message: 'action "update" requires "id".' }; return; }
|
|
129
129
|
if (a.conditions !== undefined && !validConditions(a.conditions)) { yield { type: 'error', message: '"conditions" must be [{ kind, rule }].' }; return; }
|
|
130
130
|
if (a.tool !== undefined && typeof a.tool !== 'string') { yield { type: 'error', message: '"tool" must be a string.' }; return; }
|
|
131
|
+
const priorTool = (a.tool !== undefined || a.params !== undefined)
|
|
132
|
+
? (await manager.get(a.id))?.invoke.tool ?? ''
|
|
133
|
+
: '';
|
|
131
134
|
const t = await manager.update(a.id, {
|
|
132
135
|
...(a.conditions !== undefined ? { conditions: a.conditions } : {}),
|
|
133
136
|
...(a.tool !== undefined || a.params !== undefined
|
|
134
|
-
? { invoke: { tool: a.tool ??
|
|
137
|
+
? { invoke: { tool: a.tool ?? priorTool, ...(a.params !== undefined ? { params: a.params } : {}) } }
|
|
135
138
|
: {}),
|
|
136
139
|
...(a.enabled !== undefined ? { enabled: a.enabled } : {}),
|
|
137
140
|
});
|
|
@@ -168,7 +171,7 @@ export function createTriggerActionTool(manager: TriggerManager): Tool<ToolResul
|
|
|
168
171
|
if (a.tool === undefined && a.params === undefined) { yield { type: 'error', message: 'action "move" requires a filter ("tool" and/or "params") selecting which triggers to re-target — refusing to move every trigger.' }; return; }
|
|
169
172
|
const invoke = { tool: a.toTool, ...(a.toParams !== undefined ? { params: a.toParams } : {}) };
|
|
170
173
|
const triggers: Trigger[] = [];
|
|
171
|
-
for (const t of manager.query(queryFilter(a))) {
|
|
174
|
+
for (const t of await manager.query(queryFilter(a))) {
|
|
172
175
|
const updated = await manager.update(t.id, { invoke });
|
|
173
176
|
if (updated !== undefined) triggers.push(updated);
|
|
174
177
|
}
|
|
@@ -182,7 +185,7 @@ export function createTriggerActionTool(manager: TriggerManager): Tool<ToolResul
|
|
|
182
185
|
if (a.tool === undefined && a.params === undefined) { yield { type: 'error', message: 'action "copy" requires a filter ("tool" and/or "params") selecting which triggers to duplicate — refusing to duplicate every trigger.' }; return; }
|
|
183
186
|
const invoke = { tool: a.toTool, ...(a.toParams !== undefined ? { params: a.toParams } : {}) };
|
|
184
187
|
const triggers: Trigger[] = [];
|
|
185
|
-
for (const t of manager.query(queryFilter(a))) {
|
|
188
|
+
for (const t of await manager.query(queryFilter(a))) {
|
|
186
189
|
triggers.push(await manager.add({
|
|
187
190
|
conditions: t.conditions,
|
|
188
191
|
invoke,
|
package/src/types.ts
CHANGED
|
@@ -92,11 +92,11 @@ export interface TriggerSpec {
|
|
|
92
92
|
* triggers plugin, which owns the hooks that drive them.
|
|
93
93
|
*/
|
|
94
94
|
export interface Triggers {
|
|
95
|
-
all(): Trigger[]
|
|
96
|
-
get(id: string): Trigger | undefined
|
|
95
|
+
all(): Promise<Trigger[]>;
|
|
96
|
+
get(id: string): Promise<Trigger | undefined>;
|
|
97
97
|
/** Triggers whose invocation matches the filter — `tool` (if given) equals `invoke.tool`, `params`
|
|
98
98
|
* (if given) deep-equals `invoke.params`. The "which trigger(s) fire tool X" lookup. */
|
|
99
|
-
query(filter: { tool?: string; params?: unknown }): Trigger[]
|
|
99
|
+
query(filter: { tool?: string; params?: unknown }): Promise<Trigger[]>;
|
|
100
100
|
add(spec: TriggerSpec): Promise<Trigger>;
|
|
101
101
|
update(id: string, patch: Partial<TriggerSpec>): Promise<Trigger | undefined>;
|
|
102
102
|
remove(id: string): Promise<boolean>;
|