@matatbread/matbot-triggers 0.4.17 → 0.4.21

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 CHANGED
@@ -4,7 +4,7 @@
4
4
  "node",
5
5
  "browser"
6
6
  ],
7
- "version": "0.4.17",
7
+ "version": "0.4.21",
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.17"
36
+ "@matatbread/matbot-plugin-api": "^0.4.21"
37
37
  },
38
38
  "devDependencies": {
39
39
  "typescript": "^6.0.3",
40
- "@matatbread/matbot-plugin-api": "^0.4.17"
40
+ "@matatbread/matbot-plugin-api": "^0.4.21"
41
41
  },
42
42
  "scripts": {
43
43
  "typecheck": "tsc --noEmit"
package/src/dispatch.ts CHANGED
@@ -27,23 +27,17 @@ export function invocationParams(invoke: TriggerInvoke): unknown {
27
27
  return invoke.params ?? {};
28
28
  }
29
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
30
  /**
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.
31
+ * Check an invocation against the tool's declared input BEFORE it is stored, with the same validators the
32
+ * executor applies when the trigger fires (`tools.validate`) — so a trigger that could never run is
33
+ * refused while its author is present to fix it, rather than failing silently on some later turn. Returns
34
+ * the rejection as text, or `undefined` to accept. A tool that is not registered, or no validator, is
35
+ * accepted: a trigger naming an absent tool fails soft by design, and nothing here can check what nothing
36
+ * declares.
42
37
  */
43
38
  export async function checkInvocation(services: MatbotMachine, invoke: TriggerInvoke): Promise<string | undefined> {
44
39
  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));
40
+ const errs = await services.tools.validate?.(invoke.tool, invocationParams(invoke));
47
41
  if (errs === undefined || errs.length === 0) return undefined;
48
42
  return `params do not match tool "${invoke.tool}": ${errs.map(e => `${e.path}: ${e.message}`).join('; ')}`;
49
43
  }
package/src/manager.ts CHANGED
@@ -2,7 +2,7 @@ import type { Store, MatbotMachine } from '@matatbread/matbot-plugin-api';
2
2
  import type { Trigger, TriggerSpec, TriggerSurface, TriggerKind, Triggers, FiredCondition } from './types.js';
3
3
  import { surfaceOfKind } from './types.js';
4
4
 
5
- const MAX_MSG_CHARS = 1500;
5
+ const MAX_MSG_CHARS = 4000;
6
6
 
7
7
  // Back-compat default: installs that configured a provider literally named "skills-classifier" (the
8
8
  // former hard-coded classifier name) keep working with no migration. A `classifierProvider` setting
@@ -161,14 +161,47 @@ export class TriggerManager implements Triggers {
161
161
  'the user message and the assistant message, in chronological order and clearly labelled — ' +
162
162
  `followed by a list of conditions. Evaluate each condition against the "${subject.label}". The ` +
163
163
  `"${context.label}" is the message it is paired with; use it fully whenever a condition is ` +
164
- 'relational (refers to what was asked, answered, disputed, or repeated). Fire a condition when, ' +
165
- `reading the "${subject.label}" in light of the "${context.label}", it holds. Return ONLY a JSON ` +
166
- 'object mapping each condition id (the bracketed value) to an object {"match": true|false, ' +
167
- '"why": "<a terse fragment, at most ~8 words, citing the specific evidence — not a full sentence>"}. No other text.',
164
+ 'relational (refers to what was asked, answered, disputed, or repeated).\n\n' +
165
+ // Deliberately short. An earlier version enumerated precedence rules for MATCH vs DO NOT MATCH
166
+ // clauses, written to adjudicate the long exclusion lists the stored conditions then carried.
167
+ // That machinery had no business surviving the exclusions: it was itself a patch, and it was
168
+ // what let a specific MATCH clause overrule a general exclusion and fire on a turn about the
169
+ // mechanism it was meant to ignore. Conditions state their own scope now, so the judge needs
170
+ // only the rubric and a bias to silence.
171
+ // The doctrine the trigger_action tool already states to whoever WRITES a condition — "a rule is
172
+ // a CONDITION on the FORM or SENTIMENT of a message, NOT its topic" — was missing here, where it
173
+ // is the judge rather than the author who needs it. Its absence is what let a condition naming a
174
+ // topic be satisfied by the topic's presence; a rule meant to describe the inside of a reply was
175
+ // being matched against the outside of it.
176
+ // Three bases, because the trigger set uses all three and one condition spans two: the Inner
177
+ // Voice reads FORM and SENTIMENT, while remember_fact, the date condition and 'chez nous' read
178
+ // CONTENT. A blanket "conditions never describe topic" was false of the latter — it happened to
179
+ // test clean, but it was only true of the half of the set written most recently. Naming the
180
+ // basis each condition declares is both accurate and the thing that CONTAINS semantic latitude:
181
+ // a form condition gets none, and only a content condition gets paraphrase.
182
+ 'Every condition declares its own basis, and is judged on that basis alone:\n' +
183
+ '• FORM — the shape of the message: a challenge, a complaint, a doubt, an assertion, a ' +
184
+ 'contradiction, a report. A form condition is never satisfied merely because the message is on ' +
185
+ 'the right topic, however exactly the subject appears to line up.\n' +
186
+ '• SENTIMENT — the feeling or illocution it carries: frustration, disbelief, satisfaction, an ' +
187
+ 'apology, a promise, an admission.\n' +
188
+ '• CONTENT — a fact, a name, a number, a date, or a stated phrase. A close paraphrase counts: a ' +
189
+ 'condition about relative time is met by "an hour ago" or "last week", not only by the words it ' +
190
+ 'happens to list. Latitude stops at paraphrase of what the condition actually names — it is not ' +
191
+ 'licence to match anything merely related to it.\n' +
192
+ 'Where a condition spans more than one basis, every requirement it states still applies.\n\n' +
193
+ 'Match only on evidence actually present in the text above. A condition that stays silent costs ' +
194
+ 'nothing; one that fires wrongly is disruptive and expensive.\n\n' +
195
+ `Judge only the "${subject.label}" — the other message is context for relational conditions, ` +
196
+ 'never itself a subject to judge. Return ONLY a JSON object mapping each condition id (the ' +
197
+ 'bracketed value) to an object {"match": true|false, "why": "<a terse fragment, at most ~15 words, ' +
198
+ 'citing the specific evidence — not a full sentence>"}. No other text.',
168
199
  prompt:
169
- `${context.label} (earlier):\n${context.text === '' ? '(none)' : clip(context.text)}\n\n` +
170
- `${subject.label} (later — evaluate the conditions against THIS):\n${clip(subject.text)}\n\n` +
171
- `Conditions:\n${candidates.map(c => `[${c.key}] ${c.rule}`).join('\n')}`,
200
+ `=== ${context.label.toUpperCase()} (earlier) ===\n${context.text === '' ? '(none)' : clip(context.text)}\n\n` +
201
+ `=== ${subject.label.toUpperCase()} (later — judge the conditions against THIS) ===\n${clip(subject.text)}\n\n` +
202
+ `=== CONDITIONS (${candidates.length}) ===\n` +
203
+ candidates.map(c => `--- [${c.key}] ---\n${c.rule}`).join('\n\n') +
204
+ `\n\n=== END CONDITIONS ===`,
172
205
  });
173
206
 
174
207
  let verdicts: Record<string, unknown> = {};