@matatbread/matbot-triggers 0.2.4 → 0.2.6
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/dispatch.ts +13 -30
- package/src/tools.ts +86 -20
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"node",
|
|
5
5
|
"browser"
|
|
6
6
|
],
|
|
7
|
-
"version": "0.2.
|
|
7
|
+
"version": "0.2.6",
|
|
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.2.
|
|
36
|
+
"@matatbread/matbot-plugin-api": "^0.2.6"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"typescript": "^5.8.0",
|
|
40
|
-
"@matatbread/matbot-plugin-api": "^0.2.
|
|
40
|
+
"@matatbread/matbot-plugin-api": "^0.2.6"
|
|
41
41
|
},
|
|
42
42
|
"scripts": {
|
|
43
43
|
"typecheck": "tsc --noEmit"
|
package/src/dispatch.ts
CHANGED
|
@@ -1,15 +1,7 @@
|
|
|
1
|
-
import type { MatbotMachine, Session,
|
|
1
|
+
import type { MatbotMachine, Session, PromptFn, MessageContent } from '@matatbread/matbot-plugin-api';
|
|
2
|
+
import { invokeTool } from '@matatbread/matbot-plugin-api';
|
|
2
3
|
import type { Trigger } from './types.js';
|
|
3
4
|
|
|
4
|
-
// Fallback when the firing hook carries no interactive prompt (cron/background run, or a frontend
|
|
5
|
-
// that supplied none): a tool that tries to prompt resolves to a rejection it surfaces as a normal
|
|
6
|
-
// tool error. When the hook DOES carry a prompt (a live interactive session behind this turn), it is
|
|
7
|
-
// forwarded instead, so a trigger can invoke an interactive tool (e.g. `ask_user`) for real.
|
|
8
|
-
const rejectingPrompt: PromptFn = (((p: string | FormField): Promise<string> => {
|
|
9
|
-
const label = typeof p === 'string' ? p : p.label;
|
|
10
|
-
return Promise.reject(new Error(`Non-interactive context: cannot prompt for "${label}"`));
|
|
11
|
-
}) as PromptFn);
|
|
12
|
-
|
|
13
5
|
export interface DispatchOutcome {
|
|
14
6
|
/** Whether the tool yielded a result — i.e. whether the model should be woken with it (inject). */
|
|
15
7
|
hadResult: boolean;
|
|
@@ -44,32 +36,23 @@ export async function dispatchTrigger(
|
|
|
44
36
|
// and the worst case is "this trigger did nothing and left a trace"; sibling triggers, the hook,
|
|
45
37
|
// and the session are unaffected. (The hook dispatcher's own try/catch is a second layer behind this.)
|
|
46
38
|
try {
|
|
47
|
-
|
|
48
|
-
if (tool === null) {
|
|
39
|
+
if (services.tools.resolve(trigger.invoke.tool) === null) {
|
|
49
40
|
console.warn(`[triggers] trigger ${trigger.id} invokes unknown tool "${trigger.invoke.tool}"; skipped.`);
|
|
50
41
|
fail(`tool "${trigger.invoke.tool}" not registered`);
|
|
51
42
|
return { hadResult: false, result: undefined, markers };
|
|
52
43
|
}
|
|
53
44
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
// DRTX case — an agent reply triggers `plugin(load …)`) is interactive end-to-end, not just at
|
|
64
|
-
// the tool's own ctx.prompt calls.
|
|
65
|
-
loadPlugin: (specifier: string) => services.loadPlugin(specifier, ctx.prompt ?? rejectingPrompt),
|
|
66
|
-
unloadPlugin: (specifier: string) => services.unloadPlugin(specifier),
|
|
67
|
-
...(services.workdir !== undefined ? { workdir: services.workdir } : {}),
|
|
68
|
-
...(services.configPath !== undefined ? { configPath: services.configPath } : {}),
|
|
69
|
-
...(services.files !== undefined ? { files: services.files } : {}),
|
|
70
|
-
};
|
|
45
|
+
// The prompt (when the firing hook carries one — a live interactive session behind this turn) is
|
|
46
|
+
// forwarded so a trigger can invoke an interactive tool (e.g. `ask_user`) for real; absent, the
|
|
47
|
+
// tool runs non-interactively and a prompt attempt surfaces as a normal tool error.
|
|
48
|
+
const events = invokeTool(services, trigger.invoke.tool, trigger.invoke.params, {
|
|
49
|
+
session: ctx.session,
|
|
50
|
+
signal: ctx.signal,
|
|
51
|
+
provider: ctx.provider,
|
|
52
|
+
...(ctx.prompt !== undefined ? { prompt: ctx.prompt } : {}),
|
|
53
|
+
});
|
|
71
54
|
|
|
72
|
-
for await (const ev of
|
|
55
|
+
for await (const ev of events) {
|
|
73
56
|
if (ev.type === 'result') { result = ev.value; hadResult = true; }
|
|
74
57
|
else if (ev.type === 'marker') { markers.push({ type: 'marker', creator: ev.creator, data: ev.data }); }
|
|
75
58
|
else if (ev.type === 'error') {
|
package/src/tools.ts
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
|
-
import type { Tool, ToolExecutor,
|
|
1
|
+
import type { Tool, ToolExecutor, ToolResult, ToolResultOf, ToolContext, MatbotMachine } from '@matatbread/matbot-plugin-api';
|
|
2
2
|
import type { TriggerManager } from './manager.js';
|
|
3
|
-
import type { TriggerCondition, TriggerKind } from './types.js';
|
|
3
|
+
import type { TriggerCondition, TriggerKind, Trigger } from './types.js';
|
|
4
|
+
|
|
5
|
+
declare module '@matatbread/matbot-plugin-api' {
|
|
6
|
+
interface ToolResults {
|
|
7
|
+
// One arm per action: a caller of `invokeTool(machine, 'trigger_action', { action: '…' })` gets the
|
|
8
|
+
// matching result narrowed by the `action` it passed (see ToolResult / the multi-action note on ToolResults).
|
|
9
|
+
trigger_action:
|
|
10
|
+
| ToolResult<{ triggers: Trigger[] }, { action: 'list' }>
|
|
11
|
+
| ToolResult<{ triggers: Trigger[] }, { action: 'query' }>
|
|
12
|
+
| ToolResult<Trigger, { action: 'get' }>
|
|
13
|
+
| ToolResult<{ id: string }, { action: 'add' }>
|
|
14
|
+
| ToolResult<Trigger, { action: 'update' }>
|
|
15
|
+
| ToolResult<{ id: string }, { action: 'remove' }>
|
|
16
|
+
| ToolResult<{ triggers: Trigger[] }, { action: 'move' }>
|
|
17
|
+
| ToolResult<{ triggers: Trigger[] }, { action: 'copy' }>;
|
|
18
|
+
triggers_config:
|
|
19
|
+
| ToolResult<{ classifierProvider: string | null; available: string[] }, { action: 'get' }>
|
|
20
|
+
| ToolResult<{ classifierProvider: string }, { action: 'set' }>
|
|
21
|
+
| ToolResult<{ classifierProvider: null }, { action: 'clear' }>;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
4
24
|
|
|
5
25
|
const KINDS: readonly TriggerKind[] = ['ephemeral', 'contextual', 'retract', 'followup'];
|
|
6
26
|
const isKind = (x: unknown): x is TriggerKind => typeof x === 'string' && (KINDS as readonly string[]).includes(x);
|
|
@@ -39,7 +59,17 @@ type TriggerActionInput =
|
|
|
39
59
|
| { action: 'get'; id: string }
|
|
40
60
|
| { action: 'add'; conditions: TriggerCondition[]; tool: string; params?: unknown; enabled?: boolean }
|
|
41
61
|
| { action: 'update'; id: string; conditions?: TriggerCondition[]; tool?: string; params?: unknown; enabled?: boolean }
|
|
42
|
-
| { action: 'remove'; id: string }
|
|
62
|
+
| { action: 'remove'; id: string }
|
|
63
|
+
| { action: 'move'; tool?: string; params?: unknown; toTool: string; toParams?: unknown }
|
|
64
|
+
| { action: 'copy'; tool?: string; params?: unknown; toTool: string; toParams?: unknown };
|
|
65
|
+
|
|
66
|
+
// The query selector shared by query/move/copy: `tool`/`params` narrow the set by what a trigger invokes.
|
|
67
|
+
function queryFilter(a: { tool?: unknown; params?: unknown }): { tool?: string; params?: unknown } {
|
|
68
|
+
return {
|
|
69
|
+
...(typeof a.tool === 'string' ? { tool: a.tool } : {}),
|
|
70
|
+
...(a.params !== undefined ? { params: a.params } : {}),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
43
73
|
|
|
44
74
|
// A condition is valid if it has a recognised `kind` and a string `rule`.
|
|
45
75
|
function validConditions(x: unknown): x is TriggerCondition[] {
|
|
@@ -49,9 +79,9 @@ function validConditions(x: unknown): x is TriggerCondition[] {
|
|
|
49
79
|
typeof (c as { rule?: unknown }).rule === 'string');
|
|
50
80
|
}
|
|
51
81
|
|
|
52
|
-
export function createTriggerActionTool(manager: TriggerManager): Tool {
|
|
53
|
-
const executor: ToolExecutor = {
|
|
54
|
-
async *execute(input: unknown, _ctx: ToolContext)
|
|
82
|
+
export function createTriggerActionTool(manager: TriggerManager): Tool<ToolResultOf<'trigger_action'>> {
|
|
83
|
+
const executor: ToolExecutor<ToolResultOf<'trigger_action'>> = {
|
|
84
|
+
async *execute(input: unknown, _ctx: ToolContext) {
|
|
55
85
|
const args = input as Partial<TriggerActionInput> & { action?: string };
|
|
56
86
|
|
|
57
87
|
switch (args.action) {
|
|
@@ -62,10 +92,7 @@ export function createTriggerActionTool(manager: TriggerManager): Tool {
|
|
|
62
92
|
|
|
63
93
|
case 'query': {
|
|
64
94
|
const a = args as Extract<TriggerActionInput, { action: 'query' }>;
|
|
65
|
-
yield { type: 'result', value: { triggers: manager.query(
|
|
66
|
-
...(typeof a.tool === 'string' ? { tool: a.tool } : {}),
|
|
67
|
-
...(a.params !== undefined ? { params: a.params } : {}),
|
|
68
|
-
}) } };
|
|
95
|
+
yield { type: 'result', value: { triggers: manager.query(queryFilter(a)) } };
|
|
69
96
|
return;
|
|
70
97
|
}
|
|
71
98
|
|
|
@@ -118,8 +145,39 @@ export function createTriggerActionTool(manager: TriggerManager): Tool {
|
|
|
118
145
|
return;
|
|
119
146
|
}
|
|
120
147
|
|
|
148
|
+
case 'move': {
|
|
149
|
+
const a = args as Extract<TriggerActionInput, { action: 'move' }>;
|
|
150
|
+
if (typeof a.toTool !== 'string') { yield { type: 'error', message: 'action "move" requires a string "toTool" to re-target the selected triggers onto.' }; return; }
|
|
151
|
+
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; }
|
|
152
|
+
const invoke = { tool: a.toTool, ...(a.toParams !== undefined ? { params: a.toParams } : {}) };
|
|
153
|
+
const triggers: Trigger[] = [];
|
|
154
|
+
for (const t of manager.query(queryFilter(a))) {
|
|
155
|
+
const updated = await manager.update(t.id, { invoke });
|
|
156
|
+
if (updated !== undefined) triggers.push(updated);
|
|
157
|
+
}
|
|
158
|
+
yield { type: 'result', value: { triggers } };
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
case 'copy': {
|
|
163
|
+
const a = args as Extract<TriggerActionInput, { action: 'copy' }>;
|
|
164
|
+
if (typeof a.toTool !== 'string') { yield { type: 'error', message: 'action "copy" requires a string "toTool" to point the duplicates at.' }; return; }
|
|
165
|
+
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; }
|
|
166
|
+
const invoke = { tool: a.toTool, ...(a.toParams !== undefined ? { params: a.toParams } : {}) };
|
|
167
|
+
const triggers: Trigger[] = [];
|
|
168
|
+
for (const t of manager.query(queryFilter(a))) {
|
|
169
|
+
triggers.push(await manager.add({
|
|
170
|
+
conditions: t.conditions,
|
|
171
|
+
invoke,
|
|
172
|
+
...(t.enabled !== undefined ? { enabled: t.enabled } : {}),
|
|
173
|
+
}));
|
|
174
|
+
}
|
|
175
|
+
yield { type: 'result', value: { triggers } };
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
121
179
|
default:
|
|
122
|
-
yield { type: 'error', message: `Unknown action "${String(args.action)}". Expected one of: list, query, get, add, update, remove.` };
|
|
180
|
+
yield { type: 'error', message: `Unknown action "${String(args.action)}". Expected one of: list, query, get, add, update, remove, move, copy.` };
|
|
123
181
|
}
|
|
124
182
|
},
|
|
125
183
|
};
|
|
@@ -129,7 +187,8 @@ export function createTriggerActionTool(manager: TriggerManager): Tool {
|
|
|
129
187
|
description:
|
|
130
188
|
'Manage triggers — data-driven hooks that invoke a tool when an LLM classifier judges one of ' +
|
|
131
189
|
'their conditions matched against the current turn. Use this to list triggers, find the ones ' +
|
|
132
|
-
'that invoke a given tool (query), read one, create one, edit one by id, or
|
|
190
|
+
'that invoke a given tool (query), read one, create one, edit one by id, delete one, or bulk ' +
|
|
191
|
+
're-target a selected set onto a new tool — in place (move) or as duplicates (copy).\n\n' +
|
|
133
192
|
GUIDANCE + '\n\n' +
|
|
134
193
|
'A trigger has a stable `id` — address it by that, never by its conditions. To change one, ' +
|
|
135
194
|
'"get" or "list" first to read the id, then "update" by id.\n\n' +
|
|
@@ -143,18 +202,25 @@ export function createTriggerActionTool(manager: TriggerManager): Tool {
|
|
|
143
202
|
" | { action: 'get'; id: string } // -> the trigger\n" +
|
|
144
203
|
" | { action: 'add'; conditions: TriggerCondition[]; tool: string; params?: object; enabled?: boolean } // -> { id }\n" +
|
|
145
204
|
" | { action: 'update'; id: string; conditions?: TriggerCondition[]; tool?: string; params?: object; enabled?: boolean } // edit by id\n" +
|
|
146
|
-
" | { action: 'remove'; id: string }
|
|
147
|
-
'
|
|
205
|
+
" | { action: 'remove'; id: string } // -> { id }\n" +
|
|
206
|
+
" | { action: 'move'; tool?: string; params?: object; toTool: string; toParams?: object } // re-target the queried set's invoke to toTool/toParams in place -> { triggers: [...] }\n" +
|
|
207
|
+
" | { action: 'copy'; tool?: string; params?: object; toTool: string; toParams?: object }; // duplicate the queried set, the copies invoking toTool/toParams -> { triggers: [...] }\n" +
|
|
208
|
+
'```\n\n' +
|
|
209
|
+
'For `move`/`copy`, `tool`/`params` are the SAME selector as `query` (they pick which triggers ' +
|
|
210
|
+
'to act on); `toTool`/`toParams` are the new invocation. A filter is required — both refuse to ' +
|
|
211
|
+
'act on the entire trigger set.',
|
|
148
212
|
inputSchema: {
|
|
149
213
|
type: 'object',
|
|
150
214
|
required: ['action'],
|
|
151
215
|
properties: {
|
|
152
|
-
action: { type: 'string', enum: ['list', 'query', 'get', 'add', 'update', 'remove'], description: 'The operation to perform.' },
|
|
216
|
+
action: { type: 'string', enum: ['list', 'query', 'get', 'add', 'update', 'remove', 'move', 'copy'], description: 'The operation to perform.' },
|
|
153
217
|
id: { type: 'string', description: 'Trigger id. Required for get/update/remove.' },
|
|
154
218
|
conditions: { type: 'array', description: 'Conditions [{ kind: "ephemeral"|"contextual"|"retract"|"followup", rule: string }]. Required for add.',
|
|
155
219
|
items: { type: 'object', properties: { kind: { type: 'string', enum: ['ephemeral', 'contextual', 'retract', 'followup'] }, rule: { type: 'string' } } } },
|
|
156
|
-
tool: { type: 'string', description: '
|
|
157
|
-
params: { type: 'object', description: '
|
|
220
|
+
tool: { type: 'string', description: 'For add: the tool to invoke. For query/move/copy: the selector matching invoke.tool.' },
|
|
221
|
+
params: { type: 'object', description: 'For add: params passed verbatim as the invoked tool\'s input. For query/move/copy: the selector matching invoke.params.' },
|
|
222
|
+
toTool: { type: 'string', description: 'New tool the selected triggers should invoke. Required for move/copy.' },
|
|
223
|
+
toParams: { type: 'object', description: 'New params for the selected triggers\' invocation (move/copy).' },
|
|
158
224
|
enabled: { type: 'boolean', description: 'Set false to keep but disable the trigger.' },
|
|
159
225
|
},
|
|
160
226
|
},
|
|
@@ -168,10 +234,10 @@ export function createTriggerActionTool(manager: TriggerManager): Tool {
|
|
|
168
234
|
* own provider (so triggers work with zero config); set it to pin a small/fast model. Resolved per
|
|
169
235
|
* evaluation, so a change takes effect on the next turn without a restart.
|
|
170
236
|
*/
|
|
171
|
-
export function createTriggersConfigTool(services: MatbotMachine): Tool {
|
|
237
|
+
export function createTriggersConfigTool(services: MatbotMachine): Tool<ToolResultOf<'triggers_config'>> {
|
|
172
238
|
const KEY = 'classifierProvider';
|
|
173
|
-
const executor: ToolExecutor = {
|
|
174
|
-
async *execute(input: unknown, _ctx: ToolContext)
|
|
239
|
+
const executor: ToolExecutor<ToolResultOf<'triggers_config'>> = {
|
|
240
|
+
async *execute(input: unknown, _ctx: ToolContext) {
|
|
175
241
|
const args = input as { action?: string; provider?: string };
|
|
176
242
|
const settings = services.settings();
|
|
177
243
|
const available = [...services.providers.keys()];
|