@matatbread/matbot-triggers 0.4.7 → 0.4.14
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 +42 -5
- package/src/plugin.ts +42 -4
- package/src/tools.ts +29 -11
- package/src/types.ts +2 -1
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"node",
|
|
5
5
|
"browser"
|
|
6
6
|
],
|
|
7
|
-
"version": "0.4.
|
|
7
|
+
"version": "0.4.14",
|
|
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.4.
|
|
36
|
+
"@matatbread/matbot-plugin-api": "^0.4.14"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"typescript": "^6.0.3",
|
|
40
|
-
"@matatbread/matbot-plugin-api": "^0.4.
|
|
40
|
+
"@matatbread/matbot-plugin-api": "^0.4.14"
|
|
41
41
|
},
|
|
42
42
|
"scripts": {
|
|
43
43
|
"typecheck": "tsc --noEmit"
|
package/src/dispatch.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { MatbotMachine, Session, PromptFn, MessageContent } from '@matatbread/matbot-plugin-api';
|
|
2
2
|
import { invokeTool } from '@matatbread/matbot-plugin-api';
|
|
3
|
-
import type { Trigger } from './types.js';
|
|
3
|
+
import type { Trigger, TriggerInvoke } from './types.js';
|
|
4
4
|
|
|
5
5
|
export interface DispatchOutcome {
|
|
6
6
|
/** Whether the tool yielded a result — i.e. whether the model should be woken with it (inject). */
|
|
@@ -12,6 +12,42 @@ export interface DispatchOutcome {
|
|
|
12
12
|
markers: MessageContent[];
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/** core's `TOOL_INPUT_INVALID`, the `code` on the error event a validator's rejection yields. Spelled
|
|
16
|
+
* here rather than imported because triggers depends on plugin-api alone; it is the one error a stored
|
|
17
|
+
* trigger can only recover from by being edited, which is why it is reported to the model. */
|
|
18
|
+
export const TOOL_INPUT_INVALID = 422;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The input a trigger's tool is actually called with. An omitted `params` is `{}`, never `undefined`:
|
|
22
|
+
* every tool input is an object (the model's own call of a parameterless tool sends `{}`), so passing
|
|
23
|
+
* `undefined` made every such trigger fail validation — `remember_fact` and a `tool_function` with no
|
|
24
|
+
* arguments among them — while reading as "no params needed" to whoever wrote it.
|
|
25
|
+
*/
|
|
26
|
+
export function invocationParams(invoke: TriggerInvoke): unknown {
|
|
27
|
+
return invoke.params ?? {};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// core declares `ToolCallValidator` on MatbotServices, and triggers does not depend on core, so the key
|
|
31
|
+
// is read structurally. Same shape as core's `ToolInputValidator`.
|
|
32
|
+
interface InvocationValidator {
|
|
33
|
+
validateToolCall(tool: string, parameters: unknown): Promise<{ path: string; message: string }[] | undefined>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check an invocation against the tool's declared input BEFORE it is stored, with the same validator the
|
|
38
|
+
* executor applies when the trigger fires — so a trigger that could never run is refused while its author
|
|
39
|
+
* is present to fix it, rather than failing silently on some later turn. Returns the rejection as text,
|
|
40
|
+
* or `undefined` to accept. A tool that is not registered, or no validator, is accepted: a trigger
|
|
41
|
+
* naming an absent tool fails soft by design, and nothing here can check what nothing declares.
|
|
42
|
+
*/
|
|
43
|
+
export async function checkInvocation(services: MatbotMachine, invoke: TriggerInvoke): Promise<string | undefined> {
|
|
44
|
+
if (services.tools.resolve(invoke.tool) === null) return undefined;
|
|
45
|
+
const validator = (services as MatbotMachine & { ToolCallValidator?: InvocationValidator }).ToolCallValidator;
|
|
46
|
+
const errs = await validator?.validateToolCall(invoke.tool, invocationParams(invoke));
|
|
47
|
+
if (errs === undefined || errs.length === 0) return undefined;
|
|
48
|
+
return `params do not match tool "${invoke.tool}": ${errs.map(e => `${e.path}: ${e.message}`).join('; ')}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
15
51
|
/**
|
|
16
52
|
* Invoke a fired trigger's tool, observing its output. The observational rule decides whether the
|
|
17
53
|
* model wakes: a tool that yields a `result` is producing model-facing content (`hadResult` → the
|
|
@@ -31,10 +67,11 @@ export async function dispatchTrigger(
|
|
|
31
67
|
// The runner reframes this for the tools it runs itself (INTERRUPTED_TOOL_RESULT); a trigger's tool runs
|
|
32
68
|
// outside that loop, so the same reframing has to happen here. The trace is still recorded — a trigger
|
|
33
69
|
// that was interrupted did nothing, and a post-mortem wants to know that — just not as an error.
|
|
34
|
-
|
|
70
|
+
// `code` rides along so the next turn can tell a malformed invocation from an ordinary tool failure.
|
|
71
|
+
const fail = (error: string, code?: number): void => {
|
|
35
72
|
const data = ctx.signal.aborted
|
|
36
73
|
? { triggerId: trigger.id, tool: trigger.invoke.tool, interrupted: true }
|
|
37
|
-
: { triggerId: trigger.id, tool: trigger.invoke.tool, error };
|
|
74
|
+
: { triggerId: trigger.id, tool: trigger.invoke.tool, error, ...(code !== undefined ? { code } : {}) };
|
|
38
75
|
markers.push({ type: 'marker', creator: 'triggers', data });
|
|
39
76
|
};
|
|
40
77
|
|
|
@@ -54,7 +91,7 @@ export async function dispatchTrigger(
|
|
|
54
91
|
// The prompt (when the firing hook carries one — a live interactive session behind this turn) is
|
|
55
92
|
// forwarded so a trigger can invoke an interactive tool (e.g. `ask_user`) for real; absent, the
|
|
56
93
|
// tool runs non-interactively and a prompt attempt surfaces as a normal tool error.
|
|
57
|
-
const events = invokeTool(services, trigger.invoke.tool, trigger.invoke
|
|
94
|
+
const events = invokeTool(services, trigger.invoke.tool, invocationParams(trigger.invoke), {
|
|
58
95
|
session: ctx.session,
|
|
59
96
|
signal: ctx.signal,
|
|
60
97
|
provider: ctx.provider,
|
|
@@ -66,7 +103,7 @@ export async function dispatchTrigger(
|
|
|
66
103
|
else if (ev.type === 'marker') { markers.push({ type: 'marker', creator: ev.creator, data: ev.data }); }
|
|
67
104
|
else if (ev.type === 'error') {
|
|
68
105
|
if (!ctx.signal.aborted) console.warn(`[triggers] trigger ${trigger.id} tool "${trigger.invoke.tool}" errored: ${ev.message}`);
|
|
69
|
-
fail(ev.message);
|
|
106
|
+
fail(ev.message, ev.code);
|
|
70
107
|
}
|
|
71
108
|
}
|
|
72
109
|
} catch (e) {
|
package/src/plugin.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { PLUGIN_API_VERSION } from '@matatbread/matbot-plugin-api';
|
|
2
2
|
import type { MatbotPluginSpec, MatbotMachine, Store, Message, MessageContent, Session, PromptFn, DeferredCorrection } from '@matatbread/matbot-plugin-api';
|
|
3
3
|
import { TriggerManager } from './manager.js';
|
|
4
|
-
import { dispatchTrigger, renderResult } from './dispatch.js';
|
|
4
|
+
import { dispatchTrigger, renderResult, TOOL_INPUT_INVALID } from './dispatch.js';
|
|
5
5
|
import { createTriggerActionTool, createTriggersConfigTool } from './tools.js';
|
|
6
6
|
import type { Trigger, Triggers, FiredCondition, TriggerCooldown } from './types.js';
|
|
7
7
|
|
|
@@ -102,6 +102,36 @@ function hasCorrection(c: DeferredCorrection): boolean {
|
|
|
102
102
|
return (c.ephemeral?.length ?? 0) > 0 || (c.durable?.length ?? 0) > 0;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
/**
|
|
106
|
+
* The fired triggers whose call the tool REJECTED as malformed, rendered for the model. Such a trigger
|
|
107
|
+
* is rejected every time it fires — its stored invocation is wrong, not the turn — and the only trace
|
|
108
|
+
* was an LLM-invisible marker and a server-log warning, so nobody learned of it until someone went
|
|
109
|
+
* looking. Reported on the next genuine user turn (`from` is that user message's index), where the model
|
|
110
|
+
* can say so and repair it with `trigger_action update`. The window is the markers since the PREVIOUS
|
|
111
|
+
* genuine user message, robo resubmits included, so each firing is reported exactly once.
|
|
112
|
+
*/
|
|
113
|
+
export function invalidInvocationReport(messages: readonly Message[], from: number): MessageContent[] | undefined {
|
|
114
|
+
const lines: string[] = [];
|
|
115
|
+
for (let i = from - 1; i >= 0; i--) {
|
|
116
|
+
const m = messages[i]!;
|
|
117
|
+
if (m.role === 'user' && !m.content.every(c => c.origin === 'robo')) break;
|
|
118
|
+
for (const c of m.content) {
|
|
119
|
+
if (c.type !== 'marker' || c.creator !== 'triggers') continue;
|
|
120
|
+
const d = c.data as { triggerId?: unknown; tool?: unknown; error?: unknown; code?: unknown };
|
|
121
|
+
if (d.code !== TOOL_INPUT_INVALID || typeof d.triggerId !== 'string') continue;
|
|
122
|
+
lines.unshift(`- trigger ${d.triggerId} (invokes "${String(d.tool)}"): ${String(d.error)}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (lines.length === 0) return undefined;
|
|
126
|
+
return [{ type: 'text', text: fence(
|
|
127
|
+
'On the previous turn, these triggers fired but their tool rejected the call, so they did nothing. ' +
|
|
128
|
+
'The stored `invoke.params` do not match what the tool accepts, and will fail the same way every time ' +
|
|
129
|
+
'the trigger fires:\n' + lines.join('\n') + '\n\n' +
|
|
130
|
+
'Tell the user. If the correct params are clear from the error and the tool\'s parameters, fix the ' +
|
|
131
|
+
'trigger with `trigger_action` (action "update", with its id and the corrected "params").',
|
|
132
|
+
) }];
|
|
133
|
+
}
|
|
134
|
+
|
|
105
135
|
// The core retraction marker's creator (core/src/session-runner.ts). Hardcoded as a
|
|
106
136
|
// documented cross-package string contract — markers are keyed by a creator string, and triggers
|
|
107
137
|
// depends only on plugin-api, not core. Keep in sync with RETRACTION_CREATOR there.
|
|
@@ -295,7 +325,7 @@ export async function setupTriggers(services: MatbotMachine): Promise<TriggerMan
|
|
|
295
325
|
// swap-following proxy) on every call, so it always reflects the live backend and current partition.
|
|
296
326
|
await services.register('Triggers', manager);
|
|
297
327
|
|
|
298
|
-
services.tools.register(createTriggerActionTool(manager));
|
|
328
|
+
services.tools.register(createTriggerActionTool(manager, services));
|
|
299
329
|
services.tools.register(createTriggersConfigTool(services));
|
|
300
330
|
|
|
301
331
|
// In-flight user-phase evaluations, keyed by the turn's traceId. The `screen` hook kicks one off
|
|
@@ -375,8 +405,15 @@ export async function setupTriggers(services: MatbotMachine): Promise<TriggerMan
|
|
|
375
405
|
// No traceId ⇒ can't correlate to the followup hook that consumes the verdict; skip racing (a
|
|
376
406
|
// direct runSession caller without a traceId simply gets no user-phase triggers). In the pump —
|
|
377
407
|
// the only real entry point — traceId is always set.
|
|
408
|
+
// Rejections from the previous turn ride out ephemerally on whichever return this hook takes.
|
|
409
|
+
const report = invalidInvocationReport(ctx.session.messages, ctx.session.messages.lastIndexOf(lastUser));
|
|
410
|
+
const withReport = (eph?: MessageContent[]): { ephemeral?: MessageContent[] } => {
|
|
411
|
+
const all = [...(report ?? []), ...(eph ?? [])];
|
|
412
|
+
return all.length > 0 ? { ephemeral: all } : {};
|
|
413
|
+
};
|
|
414
|
+
|
|
378
415
|
const traceId = ctx.config.traceId;
|
|
379
|
-
if (traceId === undefined) return;
|
|
416
|
+
if (traceId === undefined) return report ? withReport() : undefined;
|
|
380
417
|
|
|
381
418
|
// Kick off classify+dispatch; do not await. The record's `ready`/`fenced` let the runner poll
|
|
382
419
|
// synchronously; `.catch` neutralises an aborted-turn rejection (the classifier shares the turn's
|
|
@@ -397,7 +434,7 @@ export async function setupTriggers(services: MatbotMachine): Promise<TriggerMan
|
|
|
397
434
|
if (rec.ready && hasCorrection(rec.correction) && !rec.claimed) {
|
|
398
435
|
rec.claimed = true;
|
|
399
436
|
return {
|
|
400
|
-
...(rec.correction.ephemeral
|
|
437
|
+
...withReport(rec.correction.ephemeral),
|
|
401
438
|
...(rec.correction.durable ? { durable: rec.correction.durable } : {}),
|
|
402
439
|
};
|
|
403
440
|
}
|
|
@@ -406,6 +443,7 @@ export async function setupTriggers(services: MatbotMachine): Promise<TriggerMan
|
|
|
406
443
|
// Race path: hand the runner a deferred that claims the correction exactly once for an in-situ
|
|
407
444
|
// restart (or a pre-generation fold). Not settled / no result / already claimed ⇒ undefined.
|
|
408
445
|
return {
|
|
446
|
+
...withReport(),
|
|
409
447
|
deferred: {
|
|
410
448
|
claim: (): DeferredCorrection | undefined => {
|
|
411
449
|
if (!rec.ready || rec.claimed || !hasCorrection(rec.correction)) return undefined;
|
package/src/tools.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Tool, ToolExecutor, ToolContract, ToolResultOf, ToolContext, MatbotMachine } from '@matatbread/matbot-plugin-api';
|
|
2
2
|
import type { TriggerManager } from './manager.js';
|
|
3
|
+
import { checkInvocation } from './dispatch.js';
|
|
3
4
|
import type { TriggerCondition, TriggerCooldown, TriggerKind, Trigger } from './types.js';
|
|
4
5
|
|
|
5
6
|
declare module '@matatbread/matbot-plugin-api' {
|
|
@@ -11,7 +12,11 @@ declare module '@matatbread/matbot-plugin-api' {
|
|
|
11
12
|
| ToolContract<{ triggers: Trigger[] }, { action: 'query'; tool?: string; params?: object }>
|
|
12
13
|
| ToolContract<Trigger, { action: 'get'; id: string }>
|
|
13
14
|
| ToolContract<{ id: string }, { action: 'add'; conditions: TriggerCondition[]; tool: string; params?: object; enabled?: boolean; cooldown?: TriggerCooldown }>
|
|
14
|
-
|
|
15
|
+
// `cooldown: null` CLEARS every limit, which an omitted `cooldown` deliberately does not (it
|
|
16
|
+
// leaves the stored one untouched). The executor has always accepted it and the web UI has always
|
|
17
|
+
// sent it; only this arm omitted it, so a documented call was untypeable — invisible until the
|
|
18
|
+
// params type was actually enforced. `add` takes no `null`: there is no prior limit to clear.
|
|
19
|
+
| ToolContract<Trigger, { action: 'update'; id: string; conditions?: TriggerCondition[]; tool?: string; params?: object; enabled?: boolean; cooldown?: TriggerCooldown | null }>
|
|
15
20
|
| ToolContract<Trigger, { action: 'disable'; id: string }>
|
|
16
21
|
| ToolContract<Trigger, { action: 'enable'; id: string }>
|
|
17
22
|
| ToolContract<{ id: string }, { action: 'remove'; id: string }>
|
|
@@ -48,7 +53,10 @@ const GUIDANCE =
|
|
|
48
53
|
'verification, so keep it and add a follow-up turn carrying the output (e.g. critique/verify the ' +
|
|
49
54
|
'existing answer — it must remain in context). Choose retract vs followup by whether a match means ' +
|
|
50
55
|
'"this is wrong" vs "look at this again".\n\n' +
|
|
51
|
-
'`invoke` names a tool and its params, run verbatim when the trigger fires.
|
|
56
|
+
'`invoke` names a tool and its params (omitted ⇒ `{}`), run verbatim when the trigger fires. The ' +
|
|
57
|
+
'params are checked against the tool\'s declared parameters when the trigger is saved, so match them ' +
|
|
58
|
+
'exactly; a trigger whose call is rejected when it fires is reported on the next turn — fix it with ' +
|
|
59
|
+
'"update". If the tool produces ' +
|
|
52
60
|
'a result, the model is woken with it; a pure side-effect tool (no result) runs silently. An ' +
|
|
53
61
|
'invoke naming a tool that is not present fails soft — the trigger simply does nothing until it ' +
|
|
54
62
|
'is. To fire a skill on a condition, invoke `skill_action` with `{ action: "use", name }` — `use` ' +
|
|
@@ -61,7 +69,8 @@ const GUIDANCE =
|
|
|
61
69
|
'consecutive turns). Absent ⇒ unlimited, which is right for most triggers: a trigger that captures ' +
|
|
62
70
|
'what the user just said SHOULD fire every turn, and rate-limiting it silently loses data. Use a ' +
|
|
63
71
|
'cool-down for triggers whose consequence is a self-directed steer (critique, verify, reconsider), ' +
|
|
64
|
-
'where matching every turn is correct but acting every turn is a spin.'
|
|
72
|
+
'where matching every turn is correct but acting every turn is a spin. On "update", omitting ' +
|
|
73
|
+
'`cooldown` leaves the stored limits untouched; pass `null` to clear them.';
|
|
65
74
|
|
|
66
75
|
type TriggerActionInput =
|
|
67
76
|
| { action: 'list' }
|
|
@@ -108,7 +117,7 @@ function validConditions(x: unknown): x is TriggerCondition[] {
|
|
|
108
117
|
typeof (c as { rule?: unknown }).rule === 'string');
|
|
109
118
|
}
|
|
110
119
|
|
|
111
|
-
export function createTriggerActionTool(manager: TriggerManager): Tool<ToolResultOf<'trigger_action'>> {
|
|
120
|
+
export function createTriggerActionTool(manager: TriggerManager, services: MatbotMachine): Tool<ToolResultOf<'trigger_action'>> {
|
|
112
121
|
const executor: ToolExecutor<ToolResultOf<'trigger_action'>> = {
|
|
113
122
|
async *execute(input: unknown, _ctx: ToolContext) {
|
|
114
123
|
const args = input as Partial<TriggerActionInput> & { action?: string };
|
|
@@ -140,9 +149,12 @@ export function createTriggerActionTool(manager: TriggerManager): Tool<ToolResul
|
|
|
140
149
|
if (a.conditions.length === 0) { yield { type: 'error', message: 'action "add" requires at least one condition.' }; return; }
|
|
141
150
|
if (typeof a.tool !== 'string') { yield { type: 'error', message: 'action "add" requires a string "tool" to invoke.' }; return; }
|
|
142
151
|
if (a.cooldown !== undefined && !validCooldown(a.cooldown)) { yield { type: 'error', message: COOLDOWN_ERR }; return; }
|
|
152
|
+
const invoke = { tool: a.tool, ...(a.params !== undefined ? { params: a.params } : {}) };
|
|
153
|
+
const refused = await checkInvocation(services, invoke);
|
|
154
|
+
if (refused !== undefined) { yield { type: 'error', message: `Trigger not added — ${refused}.` }; return; }
|
|
143
155
|
const t = await manager.add({
|
|
144
156
|
conditions: a.conditions,
|
|
145
|
-
invoke
|
|
157
|
+
invoke,
|
|
146
158
|
...(a.enabled !== undefined ? { enabled: a.enabled } : {}),
|
|
147
159
|
...(a.cooldown !== undefined ? { cooldown: a.cooldown as TriggerCooldown } : {}),
|
|
148
160
|
});
|
|
@@ -156,14 +168,16 @@ export function createTriggerActionTool(manager: TriggerManager): Tool<ToolResul
|
|
|
156
168
|
if (a.conditions !== undefined && !validConditions(a.conditions)) { yield { type: 'error', message: '"conditions" must be [{ kind, rule }].' }; return; }
|
|
157
169
|
if (a.tool !== undefined && typeof a.tool !== 'string') { yield { type: 'error', message: '"tool" must be a string.' }; return; }
|
|
158
170
|
if (a.cooldown !== undefined && a.cooldown !== null && !validCooldown(a.cooldown)) { yield { type: 'error', message: COOLDOWN_ERR + ' Pass null to remove every limit.' }; return; }
|
|
159
|
-
const
|
|
160
|
-
? (await manager.get(a.id))?.invoke.tool ?? ''
|
|
161
|
-
:
|
|
171
|
+
const invoke = a.tool !== undefined || a.params !== undefined
|
|
172
|
+
? { tool: a.tool ?? (await manager.get(a.id))?.invoke.tool ?? '', ...(a.params !== undefined ? { params: a.params } : {}) }
|
|
173
|
+
: undefined;
|
|
174
|
+
// Only an edit to the invocation is checked, so a broken trigger can still be disabled or have
|
|
175
|
+
// its conditions changed without first being repaired.
|
|
176
|
+
const refused = invoke !== undefined ? await checkInvocation(services, invoke) : undefined;
|
|
177
|
+
if (refused !== undefined) { yield { type: 'error', message: `Trigger not updated — ${refused}.` }; return; }
|
|
162
178
|
const t = await manager.update(a.id, {
|
|
163
179
|
...(a.conditions !== undefined ? { conditions: a.conditions } : {}),
|
|
164
|
-
...(
|
|
165
|
-
? { invoke: { tool: a.tool ?? priorTool, ...(a.params !== undefined ? { params: a.params } : {}) } }
|
|
166
|
-
: {}),
|
|
180
|
+
...(invoke !== undefined ? { invoke } : {}),
|
|
167
181
|
...(a.enabled !== undefined ? { enabled: a.enabled } : {}),
|
|
168
182
|
// null clears every limit; an omitted `cooldown` leaves the stored one untouched.
|
|
169
183
|
...(a.cooldown !== undefined ? { cooldown: (a.cooldown === null ? {} : a.cooldown) as TriggerCooldown } : {}),
|
|
@@ -200,6 +214,8 @@ export function createTriggerActionTool(manager: TriggerManager): Tool<ToolResul
|
|
|
200
214
|
if (typeof a.toTool !== 'string') { yield { type: 'error', message: 'action "move" requires a string "toTool" to re-target the selected triggers onto.' }; return; }
|
|
201
215
|
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; }
|
|
202
216
|
const invoke = { tool: a.toTool, ...(a.toParams !== undefined ? { params: a.toParams } : {}) };
|
|
217
|
+
const refused = await checkInvocation(services, invoke);
|
|
218
|
+
if (refused !== undefined) { yield { type: 'error', message: `No triggers moved — ${refused}.` }; return; }
|
|
203
219
|
const triggers: Trigger[] = [];
|
|
204
220
|
for (const t of await manager.query(queryFilter(a))) {
|
|
205
221
|
const updated = await manager.update(t.id, { invoke });
|
|
@@ -214,6 +230,8 @@ export function createTriggerActionTool(manager: TriggerManager): Tool<ToolResul
|
|
|
214
230
|
if (typeof a.toTool !== 'string') { yield { type: 'error', message: 'action "copy" requires a string "toTool" to point the duplicates at.' }; return; }
|
|
215
231
|
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; }
|
|
216
232
|
const invoke = { tool: a.toTool, ...(a.toParams !== undefined ? { params: a.toParams } : {}) };
|
|
233
|
+
const refused = await checkInvocation(services, invoke);
|
|
234
|
+
if (refused !== undefined) { yield { type: 'error', message: `No triggers copied — ${refused}.` }; return; }
|
|
217
235
|
const triggers: Trigger[] = [];
|
|
218
236
|
for (const t of await manager.query(queryFilter(a))) {
|
|
219
237
|
triggers.push(await manager.add({
|
package/src/types.ts
CHANGED
|
@@ -62,7 +62,8 @@ export interface FiredCondition {
|
|
|
62
62
|
why?: string;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
/** The tool call a matched trigger makes. `params` is passed verbatim as the tool's input
|
|
65
|
+
/** The tool call a matched trigger makes. `params` is passed verbatim as the tool's input; absent, the
|
|
66
|
+
* tool is called with `{}`, as the model calls a tool that takes no parameters. */
|
|
66
67
|
export interface TriggerInvoke {
|
|
67
68
|
tool: string;
|
|
68
69
|
params?: unknown;
|