@matatbread/matbot-triggers 0.4.12 → 0.4.16
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 +47 -29
- package/src/types.ts +12 -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.16",
|
|
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.16"
|
|
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.16"
|
|
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,26 +1,27 @@
|
|
|
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
|
|
3
|
+
import { checkInvocation } from './dispatch.js';
|
|
4
|
+
import type { TriggerCondition, TriggerCooldown, TriggerKind, Trigger, ReportedTrigger } from './types.js';
|
|
4
5
|
|
|
5
6
|
declare module '@matatbread/matbot-plugin-api' {
|
|
6
7
|
interface ToolContracts {
|
|
7
8
|
// One arm per action: a caller of `invokeTool(machine, 'trigger_action', { action: '…' })` gets the
|
|
8
9
|
// matching result narrowed by the `action` it passed (see ToolContract / the multi-action note on ToolContracts).
|
|
9
10
|
trigger_action:
|
|
10
|
-
| ToolContract<{ triggers:
|
|
11
|
-
| ToolContract<{ triggers:
|
|
12
|
-
| ToolContract<
|
|
11
|
+
| ToolContract<{ triggers: ReportedTrigger[] }, { action: 'list' }>
|
|
12
|
+
| ToolContract<{ triggers: ReportedTrigger[] }, { action: 'query'; tool?: string; params?: object }>
|
|
13
|
+
| ToolContract<ReportedTrigger, { 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
|
|
15
16
|
// leaves the stored one untouched). The executor has always accepted it and the web UI has always
|
|
16
17
|
// sent it; only this arm omitted it, so a documented call was untypeable — invisible until the
|
|
17
18
|
// params type was actually enforced. `add` takes no `null`: there is no prior limit to clear.
|
|
18
|
-
| ToolContract<
|
|
19
|
-
| ToolContract<
|
|
20
|
-
| ToolContract<
|
|
21
|
-
| ToolContract<{ id: string },
|
|
22
|
-
| ToolContract<{ triggers:
|
|
23
|
-
| ToolContract<{ triggers:
|
|
19
|
+
| ToolContract<ReportedTrigger, { action: 'update'; id: string; conditions?: TriggerCondition[]; tool?: string; params?: object; enabled?: boolean; cooldown?: TriggerCooldown | null }>
|
|
20
|
+
| ToolContract<ReportedTrigger, { action: 'disable'; id: string }>
|
|
21
|
+
| ToolContract<ReportedTrigger, { action: 'enable'; id: string }>
|
|
22
|
+
| ToolContract<{ id: string }, { action: 'remove'; id: string }>
|
|
23
|
+
| ToolContract<{ triggers: ReportedTrigger[] }, { action: 'move'; tool?: string; params?: object; toTool: string; toParams?: object }>
|
|
24
|
+
| ToolContract<{ triggers: ReportedTrigger[] }, { action: 'copy'; tool?: string; params?: object; toTool: string; toParams?: object }>;
|
|
24
25
|
triggers_config:
|
|
25
26
|
| ToolContract<{ classifierProvider: string | null; available: string[] }, { action: 'get' }>
|
|
26
27
|
| ToolContract<{ classifierProvider: string }, { action: 'set'; provider: string }>
|
|
@@ -52,7 +53,10 @@ const GUIDANCE =
|
|
|
52
53
|
'verification, so keep it and add a follow-up turn carrying the output (e.g. critique/verify the ' +
|
|
53
54
|
'existing answer — it must remain in context). Choose retract vs followup by whether a match means ' +
|
|
54
55
|
'"this is wrong" vs "look at this again".\n\n' +
|
|
55
|
-
'`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 ' +
|
|
56
60
|
'a result, the model is woken with it; a pure side-effect tool (no result) runs silently. An ' +
|
|
57
61
|
'invoke naming a tool that is not present fails soft — the trigger simply does nothing until it ' +
|
|
58
62
|
'is. To fire a skill on a condition, invoke `skill_action` with `{ action: "use", name }` — `use` ' +
|
|
@@ -80,6 +84,11 @@ type TriggerActionInput =
|
|
|
80
84
|
| { action: 'move'; tool?: string; params?: unknown; toTool: string; toParams?: unknown }
|
|
81
85
|
| { action: 'copy'; tool?: string; params?: unknown; toTool: string; toParams?: unknown };
|
|
82
86
|
|
|
87
|
+
// `enabled` resolved for reporting (see ReportedTrigger): it is stored only when explicitly written, so
|
|
88
|
+
// a listing otherwise carried it for some triggers and not others, leaving a reader to know "absent ⇒
|
|
89
|
+
// enabled" rather than see it. Applied at the yield sites only — the stored document is untouched.
|
|
90
|
+
const reported = (t: Trigger): ReportedTrigger => ({ ...t, enabled: t.enabled !== false });
|
|
91
|
+
|
|
83
92
|
// The query selector shared by query/move/copy: `tool`/`params` narrow the set by what a trigger invokes.
|
|
84
93
|
function queryFilter(a: { tool?: unknown; params?: unknown }): { tool?: string; params?: unknown } {
|
|
85
94
|
return {
|
|
@@ -113,20 +122,20 @@ function validConditions(x: unknown): x is TriggerCondition[] {
|
|
|
113
122
|
typeof (c as { rule?: unknown }).rule === 'string');
|
|
114
123
|
}
|
|
115
124
|
|
|
116
|
-
export function createTriggerActionTool(manager: TriggerManager): Tool<ToolResultOf<'trigger_action'>> {
|
|
125
|
+
export function createTriggerActionTool(manager: TriggerManager, services: MatbotMachine): Tool<ToolResultOf<'trigger_action'>> {
|
|
117
126
|
const executor: ToolExecutor<ToolResultOf<'trigger_action'>> = {
|
|
118
127
|
async *execute(input: unknown, _ctx: ToolContext) {
|
|
119
128
|
const args = input as Partial<TriggerActionInput> & { action?: string };
|
|
120
129
|
|
|
121
130
|
switch (args.action) {
|
|
122
131
|
case 'list': {
|
|
123
|
-
yield { type: 'result', value: { triggers: await manager.all() } };
|
|
132
|
+
yield { type: 'result', value: { triggers: (await manager.all()).map(reported) } };
|
|
124
133
|
return;
|
|
125
134
|
}
|
|
126
135
|
|
|
127
136
|
case 'query': {
|
|
128
137
|
const a = args as Extract<TriggerActionInput, { action: 'query' }>;
|
|
129
|
-
yield { type: 'result', value: { triggers: await manager.query(queryFilter(a)) } };
|
|
138
|
+
yield { type: 'result', value: { triggers: (await manager.query(queryFilter(a))).map(reported) } };
|
|
130
139
|
return;
|
|
131
140
|
}
|
|
132
141
|
|
|
@@ -135,7 +144,7 @@ export function createTriggerActionTool(manager: TriggerManager): Tool<ToolResul
|
|
|
135
144
|
if (!id) { yield { type: 'error', message: 'action "get" requires "id".' }; return; }
|
|
136
145
|
const t = await manager.get(id);
|
|
137
146
|
if (!t) { yield { type: 'error', message: `Trigger not found: "${id}"` }; return; }
|
|
138
|
-
yield { type: 'result', value: t };
|
|
147
|
+
yield { type: 'result', value: reported(t) };
|
|
139
148
|
return;
|
|
140
149
|
}
|
|
141
150
|
|
|
@@ -145,9 +154,12 @@ export function createTriggerActionTool(manager: TriggerManager): Tool<ToolResul
|
|
|
145
154
|
if (a.conditions.length === 0) { yield { type: 'error', message: 'action "add" requires at least one condition.' }; return; }
|
|
146
155
|
if (typeof a.tool !== 'string') { yield { type: 'error', message: 'action "add" requires a string "tool" to invoke.' }; return; }
|
|
147
156
|
if (a.cooldown !== undefined && !validCooldown(a.cooldown)) { yield { type: 'error', message: COOLDOWN_ERR }; return; }
|
|
157
|
+
const invoke = { tool: a.tool, ...(a.params !== undefined ? { params: a.params } : {}) };
|
|
158
|
+
const refused = await checkInvocation(services, invoke);
|
|
159
|
+
if (refused !== undefined) { yield { type: 'error', message: `Trigger not added — ${refused}.` }; return; }
|
|
148
160
|
const t = await manager.add({
|
|
149
161
|
conditions: a.conditions,
|
|
150
|
-
invoke
|
|
162
|
+
invoke,
|
|
151
163
|
...(a.enabled !== undefined ? { enabled: a.enabled } : {}),
|
|
152
164
|
...(a.cooldown !== undefined ? { cooldown: a.cooldown as TriggerCooldown } : {}),
|
|
153
165
|
});
|
|
@@ -161,20 +173,22 @@ export function createTriggerActionTool(manager: TriggerManager): Tool<ToolResul
|
|
|
161
173
|
if (a.conditions !== undefined && !validConditions(a.conditions)) { yield { type: 'error', message: '"conditions" must be [{ kind, rule }].' }; return; }
|
|
162
174
|
if (a.tool !== undefined && typeof a.tool !== 'string') { yield { type: 'error', message: '"tool" must be a string.' }; return; }
|
|
163
175
|
if (a.cooldown !== undefined && a.cooldown !== null && !validCooldown(a.cooldown)) { yield { type: 'error', message: COOLDOWN_ERR + ' Pass null to remove every limit.' }; return; }
|
|
164
|
-
const
|
|
165
|
-
? (await manager.get(a.id))?.invoke.tool ?? ''
|
|
166
|
-
:
|
|
176
|
+
const invoke = a.tool !== undefined || a.params !== undefined
|
|
177
|
+
? { tool: a.tool ?? (await manager.get(a.id))?.invoke.tool ?? '', ...(a.params !== undefined ? { params: a.params } : {}) }
|
|
178
|
+
: undefined;
|
|
179
|
+
// Only an edit to the invocation is checked, so a broken trigger can still be disabled or have
|
|
180
|
+
// its conditions changed without first being repaired.
|
|
181
|
+
const refused = invoke !== undefined ? await checkInvocation(services, invoke) : undefined;
|
|
182
|
+
if (refused !== undefined) { yield { type: 'error', message: `Trigger not updated — ${refused}.` }; return; }
|
|
167
183
|
const t = await manager.update(a.id, {
|
|
168
184
|
...(a.conditions !== undefined ? { conditions: a.conditions } : {}),
|
|
169
|
-
...(
|
|
170
|
-
? { invoke: { tool: a.tool ?? priorTool, ...(a.params !== undefined ? { params: a.params } : {}) } }
|
|
171
|
-
: {}),
|
|
185
|
+
...(invoke !== undefined ? { invoke } : {}),
|
|
172
186
|
...(a.enabled !== undefined ? { enabled: a.enabled } : {}),
|
|
173
187
|
// null clears every limit; an omitted `cooldown` leaves the stored one untouched.
|
|
174
188
|
...(a.cooldown !== undefined ? { cooldown: (a.cooldown === null ? {} : a.cooldown) as TriggerCooldown } : {}),
|
|
175
189
|
});
|
|
176
190
|
if (t === undefined) { yield { type: 'error', message: `Trigger not found: "${a.id}"` }; return; }
|
|
177
|
-
yield { type: 'result', value: t };
|
|
191
|
+
yield { type: 'result', value: reported(t) };
|
|
178
192
|
return;
|
|
179
193
|
}
|
|
180
194
|
|
|
@@ -187,7 +201,7 @@ export function createTriggerActionTool(manager: TriggerManager): Tool<ToolResul
|
|
|
187
201
|
if (!id) { yield { type: 'error', message: `action "${args.action}" requires "id".` }; return; }
|
|
188
202
|
const t = await manager.update(id, { enabled: args.action === 'enable' });
|
|
189
203
|
if (t === undefined) { yield { type: 'error', message: `Trigger not found: "${id}"` }; return; }
|
|
190
|
-
yield { type: 'result', value: t };
|
|
204
|
+
yield { type: 'result', value: reported(t) };
|
|
191
205
|
return;
|
|
192
206
|
}
|
|
193
207
|
|
|
@@ -205,10 +219,12 @@ export function createTriggerActionTool(manager: TriggerManager): Tool<ToolResul
|
|
|
205
219
|
if (typeof a.toTool !== 'string') { yield { type: 'error', message: 'action "move" requires a string "toTool" to re-target the selected triggers onto.' }; return; }
|
|
206
220
|
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; }
|
|
207
221
|
const invoke = { tool: a.toTool, ...(a.toParams !== undefined ? { params: a.toParams } : {}) };
|
|
208
|
-
const
|
|
222
|
+
const refused = await checkInvocation(services, invoke);
|
|
223
|
+
if (refused !== undefined) { yield { type: 'error', message: `No triggers moved — ${refused}.` }; return; }
|
|
224
|
+
const triggers: ReportedTrigger[] = [];
|
|
209
225
|
for (const t of await manager.query(queryFilter(a))) {
|
|
210
226
|
const updated = await manager.update(t.id, { invoke });
|
|
211
|
-
if (updated !== undefined) triggers.push(updated);
|
|
227
|
+
if (updated !== undefined) triggers.push(reported(updated));
|
|
212
228
|
}
|
|
213
229
|
yield { type: 'result', value: { triggers } };
|
|
214
230
|
return;
|
|
@@ -219,14 +235,16 @@ export function createTriggerActionTool(manager: TriggerManager): Tool<ToolResul
|
|
|
219
235
|
if (typeof a.toTool !== 'string') { yield { type: 'error', message: 'action "copy" requires a string "toTool" to point the duplicates at.' }; return; }
|
|
220
236
|
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; }
|
|
221
237
|
const invoke = { tool: a.toTool, ...(a.toParams !== undefined ? { params: a.toParams } : {}) };
|
|
222
|
-
const
|
|
238
|
+
const refused = await checkInvocation(services, invoke);
|
|
239
|
+
if (refused !== undefined) { yield { type: 'error', message: `No triggers copied — ${refused}.` }; return; }
|
|
240
|
+
const triggers: ReportedTrigger[] = [];
|
|
223
241
|
for (const t of await manager.query(queryFilter(a))) {
|
|
224
|
-
triggers.push(await manager.add({
|
|
242
|
+
triggers.push(reported(await manager.add({
|
|
225
243
|
conditions: t.conditions,
|
|
226
244
|
invoke,
|
|
227
245
|
...(t.enabled !== undefined ? { enabled: t.enabled } : {}),
|
|
228
246
|
...(t.cooldown !== undefined ? { cooldown: t.cooldown } : {}),
|
|
229
|
-
}));
|
|
247
|
+
})));
|
|
230
248
|
}
|
|
231
249
|
yield { type: 'result', value: { triggers } };
|
|
232
250
|
return;
|
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;
|
|
@@ -106,6 +107,16 @@ export interface Trigger {
|
|
|
106
107
|
updatedAt: string;
|
|
107
108
|
}
|
|
108
109
|
|
|
110
|
+
/**
|
|
111
|
+
* A trigger as `trigger_action` REPORTS it: `enabled` resolved rather than omitted.
|
|
112
|
+
*
|
|
113
|
+
* Stored documents carry `enabled` only when it was explicitly written, so a listing showed it for some
|
|
114
|
+
* triggers and not others — and "absent ⇒ enabled" is a rule a reader has to know rather than see. The
|
|
115
|
+
* resolution happens at the tool boundary only: stored shape and {@link Triggers} consumers are
|
|
116
|
+
* untouched, so no write can persist a value nobody asked for.
|
|
117
|
+
*/
|
|
118
|
+
export type ReportedTrigger = Omit<Trigger, 'enabled'> & { enabled: boolean };
|
|
119
|
+
|
|
109
120
|
/** Fields a caller supplies when creating or replacing a trigger; identity/versioning is the store's. */
|
|
110
121
|
export interface TriggerSpec {
|
|
111
122
|
conditions: TriggerCondition[];
|