@chatpanel/events 0.28.0 → 0.29.0
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 +1 -1
- package/voice-intents.js +52 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatpanel/events",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "The canonical ChatPanel event-log and capability contracts \u2014 typed durable facts, clock-free deterministic linearization, schema upcasting, and the invariants the replay harness asserts. Pure, dependency-free ESM shared by the ChatPanel extension, gateway and bridge.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
package/voice-intents.js
CHANGED
|
@@ -380,7 +380,7 @@ export const REFINEMENT_SCHEMA = defineSchema({
|
|
|
380
380
|
name: { type: 'string', max: 48, describe: 'a label of at most 6 words' },
|
|
381
381
|
kind: {
|
|
382
382
|
type: 'enum',
|
|
383
|
-
values: ['question', 'monitor', 'note', 'skill', 'none'],
|
|
383
|
+
values: ['question', 'monitor', 'note', 'skill', 'timer', 'none'],
|
|
384
384
|
// An unknown kind becomes a QUESTION — the least surprising thing to do with something
|
|
385
385
|
// someone asked for, and the only kind that is undone by ignoring the answer. Guessing
|
|
386
386
|
// "monitor" instead would leave a card watching the meeting that nobody asked for.
|
|
@@ -409,6 +409,7 @@ export function refinementPrompt(utterance) {
|
|
|
409
409
|
' "keep an eye on"). A one-off question is NOT a monitor.',
|
|
410
410
|
' note — they asked for notes written down ("take notes on", "write that up").',
|
|
411
411
|
' skill — they named a saved skill ("use the summarize skill"); put its name in `skill`.',
|
|
412
|
+
' timer — alerted after an AMOUNT OF TIME ("set a one minute"); keep it in `request`.',
|
|
412
413
|
' none — not asking for anything: thinking aloud, or talking ABOUT the assistant.',
|
|
413
414
|
'Never invent a request that is not there — return "none". Keep `request` close to their',
|
|
414
415
|
'words; do not answer it.',
|
|
@@ -454,8 +455,23 @@ export function settleRefinement(v) {
|
|
|
454
455
|
const request = String(v.request || '').trim();
|
|
455
456
|
if (v.kind === 'none' || !request) return { request: '', name: '', kind: 'none', skill: '' };
|
|
456
457
|
// A "skill" with no name is a question — there is nothing to run.
|
|
457
|
-
|
|
458
|
-
|
|
458
|
+
let kind = v.kind === 'skill' && !v.skill ? 'question' : v.kind;
|
|
459
|
+
const name = String(v.name || '').trim() || request;
|
|
460
|
+
// A TIMER IS RESOLVED HERE, not by whatever runs the request.
|
|
461
|
+
//
|
|
462
|
+
// A spoken timer the grammar missed used to arrive as a plain question, so it went to the
|
|
463
|
+
// chat — where an agent answered it by running `sleep 60` in its own sandbox and saying it
|
|
464
|
+
// would notify. It cannot: nothing connects that process back to the user. Reading the
|
|
465
|
+
// duration here turns it back into a job the product itself owns and can fire.
|
|
466
|
+
//
|
|
467
|
+
// No duration means the model called it a timer without one, and a timer with no duration
|
|
468
|
+
// is a question about time. Downgraded rather than dropped.
|
|
469
|
+
if (kind === 'timer') {
|
|
470
|
+
const d = parseDuration(request);
|
|
471
|
+
if (!d) return { request, name, kind: 'question', skill: '' };
|
|
472
|
+
return { request, name, kind: 'timer', skill: '', ms: d.ms };
|
|
473
|
+
}
|
|
474
|
+
return { request, name, kind, skill: v.skill || '' };
|
|
459
475
|
}
|
|
460
476
|
|
|
461
477
|
/**
|
|
@@ -608,13 +624,21 @@ const FRACTION = { half: 0.5, quarter: 0.25 };
|
|
|
608
624
|
export function parseNumberWords(words) {
|
|
609
625
|
if (!words.length) return null;
|
|
610
626
|
let total = null;
|
|
627
|
+
// THE TWO-MINUTE ONE-MINUTE TIMER. The article used to set the count to 1 outright, and
|
|
628
|
+
// everything after it ADDS — so "set a one minute timer", which is how most people say it,
|
|
629
|
+
// came out as 1 + 1 = two minutes. Reported as "I asked for a 1-minute timer, it didn't
|
|
630
|
+
// work": it worked, twice as long, which looks exactly like not working.
|
|
631
|
+
//
|
|
632
|
+
// The article is now only a count when nothing else supplies one. "A minute" is still a
|
|
633
|
+
// minute; "a one minute" is one minute, not two.
|
|
634
|
+
let article = false;
|
|
611
635
|
for (let i = 0; i < words.length; i++) {
|
|
612
636
|
const w = words[i];
|
|
613
637
|
if (w === 'and' || w === 'of') continue; // "two AND a half", "a quarter OF an hour"
|
|
614
638
|
// "an hour" is one hour. "A quarter of an hour" is a quarter, and "two and A half" is
|
|
615
639
|
// 2.5 — in both of those the article belongs to the fraction, not to the count.
|
|
616
640
|
if (w === 'a' || w === 'an') {
|
|
617
|
-
if (total === null && !(words[i + 1] in FRACTION))
|
|
641
|
+
if (total === null && !(words[i + 1] in FRACTION)) article = true;
|
|
618
642
|
continue;
|
|
619
643
|
}
|
|
620
644
|
if (w in FRACTION) { total = (total ?? 0) + FRACTION[w]; continue; }
|
|
@@ -623,7 +647,7 @@ export function parseNumberWords(words) {
|
|
|
623
647
|
if (/^\d+(?:\.\d+)?$/.test(w)) { total = (total ?? 0) + Number(w); continue; }
|
|
624
648
|
return null;
|
|
625
649
|
}
|
|
626
|
-
return total;
|
|
650
|
+
return total ?? (article ? 1 : null);
|
|
627
651
|
}
|
|
628
652
|
|
|
629
653
|
const UNIT_MS = {
|
|
@@ -927,9 +951,23 @@ export const timerIntent = defineVoiceIntent({
|
|
|
927
951
|
description: 'Starts a countdown and alerts when it finishes.',
|
|
928
952
|
examples: ['set a timer for 10 minutes', 'start a 90 second timer', 'timer for an hour and a half'],
|
|
929
953
|
match: (command, { now = Date.now() } = {}) => {
|
|
930
|
-
if (!/\btimers?\b/i.test(command)) return null;
|
|
931
954
|
const d = parseDuration(command);
|
|
932
955
|
if (!d) return null;
|
|
956
|
+
// THE MISSING HEAD NOUN. "Okay ChatPanel, set a 1-minute timer" reached the scanner as
|
|
957
|
+
// "set a one minute." — the word this pattern was keyed on was still being said. With no
|
|
958
|
+
// intent it went to the model, which answered a spoken timer by running `sleep 60` in a
|
|
959
|
+
// sandbox and promising a notification it had no way to deliver.
|
|
960
|
+
//
|
|
961
|
+
// So a SET verb whose only content is a duration is a timer: nothing else is ever said
|
|
962
|
+
// that way. Anything left over after the duration and the plumbing words means it is
|
|
963
|
+
// something else — "set a 5 minute meeting" is a meeting — and still needs the noun.
|
|
964
|
+
if (!/\btimers?\b/i.test(command)) {
|
|
965
|
+
if (!/^(?:set|start|make|create|put)\b/i.test(command.trim())) return null;
|
|
966
|
+
const rest = (command.slice(0, d.start) + ' ' + command.slice(d.end))
|
|
967
|
+
.replace(/\b(set|start|make|create|put|a|an|the|for|please|to|of|and|half|quarter|this|that|time|up|on|me)\b/gi, ' ')
|
|
968
|
+
.replace(/[^\p{L}\p{N}]+/gu, '');
|
|
969
|
+
if (rest) return null;
|
|
970
|
+
}
|
|
933
971
|
// "timer for the standup" — whatever is left once the duration and the plumbing words
|
|
934
972
|
// are removed is what the timer is FOR, and a labelled timer is the difference between
|
|
935
973
|
// three anonymous countdowns and three useful ones.
|
|
@@ -967,7 +1005,10 @@ export const noteIntent = defineVoiceIntent({
|
|
|
967
1005
|
description: 'Appends a line to the meeting notes.',
|
|
968
1006
|
examples: ['note that we agreed to ship on Friday', 'take a note: budget is approved'],
|
|
969
1007
|
match: (command) => {
|
|
970
|
-
|
|
1008
|
+
// "Take THE NOTES of whatever we spoke so far" — the plural, the definite article and
|
|
1009
|
+
// "of" instead of "that" were all misses, so the request went to the model, which spent
|
|
1010
|
+
// four tool calls hunting for a transcript before writing anything.
|
|
1011
|
+
const m = /^(?:(?:take|make|write|jot|add)\s+(?:down\s+)?(?:a\s+|the\s+|some\s+)?notes?|(?:write|jot)\s+down|notes?)\b[\s:,-]*(?:down\s+)?(?:that\s+|of\s+|on\s+|about\s+|from\s+)?(.+)$/i.exec(command.trim());
|
|
971
1012
|
const text = m && tidy(m[1]);
|
|
972
1013
|
return text ? { text } : null;
|
|
973
1014
|
},
|
|
@@ -980,7 +1021,10 @@ export const monitorIntent = defineVoiceIntent({
|
|
|
980
1021
|
examples: ['watch for whether we agree a date', 'keep an eye on the pricing question', 'track who owns the migration'],
|
|
981
1022
|
classUsed: 'C', // it starts model turns for the rest of the meeting — say so
|
|
982
1023
|
match: (command) => {
|
|
983
|
-
|
|
1024
|
+
// "Start a live monitor about…" is how it was asked for in the very demo of the feature,
|
|
1025
|
+
// and it matched nothing: every pattern here began at the verb, so the noun form —
|
|
1026
|
+
// start/set up a monitor — fell through to the model and no card was ever created.
|
|
1027
|
+
const m = /^(?:(?:start|set\s+up|create|begin|add|open|run)\s+(?:a\s+|the\s+|an\s+)?(?:live\s+|new\s+)?(?:monitor|monitoring|watch|tracker)|watch\s+(?:out\s+)?for|watch|keep\s+an\s+eye\s+on|track|monitor|monitoring)\b[\s:,-]*(?:whether\s+|if\s+|for\s+|on\s+|about\s+|that\s+)?(.+)$/i.exec(command.trim());
|
|
984
1028
|
const prompt = m && tidy(m[1]);
|
|
985
1029
|
return prompt && prompt.length > 2 ? { prompt } : null;
|
|
986
1030
|
},
|