@animalabs/agent-framework 0.27.0 → 0.28.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/README.md +56 -15
- package/dist/src/agent.d.ts +29 -2
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +69 -11
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/server.d.ts.map +1 -1
- package/dist/src/api/server.js +4 -2
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/auto-continue.d.ts +3 -2
- package/dist/src/auto-continue.d.ts.map +1 -1
- package/dist/src/auto-continue.js +4 -3
- package/dist/src/auto-continue.js.map +1 -1
- package/dist/src/cut-off-round.d.ts +24 -0
- package/dist/src/cut-off-round.d.ts.map +1 -0
- package/dist/src/cut-off-round.js +87 -0
- package/dist/src/cut-off-round.js.map +1 -0
- package/dist/src/framework.d.ts +304 -89
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +2577 -678
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +99 -3
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +538 -13
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/index.d.ts +2 -1
- package/dist/src/gate/index.d.ts.map +1 -1
- package/dist/src/gate/index.js +1 -0
- package/dist/src/gate/index.js.map +1 -1
- package/dist/src/gate/reply-judge.d.ts +116 -0
- package/dist/src/gate/reply-judge.d.ts.map +1 -0
- package/dist/src/gate/reply-judge.js +467 -0
- package/dist/src/gate/reply-judge.js.map +1 -0
- package/dist/src/gate/types.d.ts +161 -0
- package/dist/src/gate/types.d.ts.map +1 -1
- package/dist/src/heartbeat/heartbeat.d.ts +184 -0
- package/dist/src/heartbeat/heartbeat.d.ts.map +1 -0
- package/dist/src/heartbeat/heartbeat.js +752 -0
- package/dist/src/heartbeat/heartbeat.js.map +1 -0
- package/dist/src/heartbeat/reminder-time.d.ts +5 -0
- package/dist/src/heartbeat/reminder-time.d.ts.map +1 -0
- package/dist/src/heartbeat/reminder-time.js +107 -0
- package/dist/src/heartbeat/reminder-time.js.map +1 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/capability-mask.d.ts.map +1 -1
- package/dist/src/mcpl/capability-mask.js +3 -2
- package/dist/src/mcpl/capability-mask.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +2 -1
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/inbound-source.d.ts +26 -10
- package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
- package/dist/src/mcpl/inbound-source.js +40 -69
- package/dist/src/mcpl/inbound-source.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +18 -8
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +50 -10
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/tags.d.ts +9 -2
- package/dist/src/mcpl/tags.d.ts.map +1 -1
- package/dist/src/mcpl/tags.js +12 -2
- package/dist/src/mcpl/tags.js.map +1 -1
- package/dist/src/mcpl/tool-presence.d.ts +66 -14
- package/dist/src/mcpl/tool-presence.d.ts.map +1 -1
- package/dist/src/mcpl/tool-presence.js +134 -65
- package/dist/src/mcpl/tool-presence.js.map +1 -1
- package/dist/src/mcpl/tool-signature.d.ts +10 -3
- package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
- package/dist/src/mcpl/tool-signature.js +47 -18
- package/dist/src/mcpl/tool-signature.js.map +1 -1
- package/dist/src/mcpl/toolset-host.d.ts +88 -37
- package/dist/src/mcpl/toolset-host.d.ts.map +1 -1
- package/dist/src/mcpl/toolset-host.js +207 -108
- package/dist/src/mcpl/toolset-host.js.map +1 -1
- package/dist/src/mcpl/toolsets-module.d.ts +14 -11
- package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
- package/dist/src/mcpl/toolsets-module.js +141 -49
- package/dist/src/mcpl/toolsets-module.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +3 -1
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/module-registry.d.ts +4 -1
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +9 -2
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/history/index.d.ts.map +1 -1
- package/dist/src/modules/history/index.js +22 -11
- package/dist/src/modules/history/index.js.map +1 -1
- package/dist/src/modules/workspace/answers.d.ts +22 -0
- package/dist/src/modules/workspace/answers.d.ts.map +1 -0
- package/dist/src/modules/workspace/answers.js +123 -0
- package/dist/src/modules/workspace/answers.js.map +1 -0
- package/dist/src/modules/workspace/branch-intent.d.ts +5 -3
- package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -1
- package/dist/src/modules/workspace/branch-intent.js +6 -2
- package/dist/src/modules/workspace/branch-intent.js.map +1 -1
- package/dist/src/modules/workspace/direct-write.d.ts +3 -0
- package/dist/src/modules/workspace/direct-write.d.ts.map +1 -1
- package/dist/src/modules/workspace/direct-write.js +57 -24
- package/dist/src/modules/workspace/direct-write.js.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.d.ts +33 -11
- package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.js +48 -17
- package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
- package/dist/src/modules/workspace/edit-text.d.ts +2 -1
- package/dist/src/modules/workspace/edit-text.d.ts.map +1 -1
- package/dist/src/modules/workspace/edit-text.js +4 -2
- package/dist/src/modules/workspace/edit-text.js.map +1 -1
- package/dist/src/modules/workspace/effects.d.ts.map +1 -1
- package/dist/src/modules/workspace/effects.js +20 -13
- package/dist/src/modules/workspace/effects.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +62 -51
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +934 -531
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +75 -63
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +366 -180
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/read-page.d.ts.map +1 -1
- package/dist/src/modules/workspace/read-page.js +7 -1
- package/dist/src/modules/workspace/read-page.js.map +1 -1
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +26 -16
- package/dist/src/modules/workspace/reconcile.js.map +1 -1
- package/dist/src/modules/workspace/result-text.d.ts +1 -0
- package/dist/src/modules/workspace/result-text.d.ts.map +1 -1
- package/dist/src/modules/workspace/result-text.js.map +1 -1
- package/dist/src/modules/workspace/sync.d.ts +9 -0
- package/dist/src/modules/workspace/sync.d.ts.map +1 -1
- package/dist/src/modules/workspace/sync.js +14 -0
- package/dist/src/modules/workspace/sync.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +96 -43
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
- package/dist/src/modules/workspace/watcher.js +8 -3
- package/dist/src/modules/workspace/watcher.js.map +1 -1
- package/dist/src/prose-drafts.d.ts +4 -3
- package/dist/src/prose-drafts.d.ts.map +1 -1
- package/dist/src/prose-drafts.js +3 -2
- package/dist/src/prose-drafts.js.map +1 -1
- package/dist/src/reasoning-loss-notice.d.ts +23 -0
- package/dist/src/reasoning-loss-notice.d.ts.map +1 -0
- package/dist/src/reasoning-loss-notice.js +50 -0
- package/dist/src/reasoning-loss-notice.js.map +1 -0
- package/dist/src/reasoning-loss.d.ts +24 -0
- package/dist/src/reasoning-loss.d.ts.map +1 -0
- package/dist/src/reasoning-loss.js +102 -0
- package/dist/src/reasoning-loss.js.map +1 -0
- package/dist/src/send-tools.d.ts +3 -2
- package/dist/src/send-tools.d.ts.map +1 -1
- package/dist/src/send-tools.js +3 -2
- package/dist/src/send-tools.js.map +1 -1
- package/dist/src/sent-rows.js +1 -1
- package/dist/src/thinking-drops.d.ts +4 -11
- package/dist/src/thinking-drops.d.ts.map +1 -1
- package/dist/src/thinking-drops.js +7 -14
- package/dist/src/thinking-drops.js.map +1 -1
- package/dist/src/tool-args.d.ts +12 -0
- package/dist/src/tool-args.d.ts.map +1 -1
- package/dist/src/tool-args.js +35 -1
- package/dist/src/tool-args.js.map +1 -1
- package/dist/src/tool-result-guard.d.ts +8 -4
- package/dist/src/tool-result-guard.d.ts.map +1 -1
- package/dist/src/tool-result-guard.js +23 -11
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tune-out/tools.d.ts +4 -1
- package/dist/src/tune-out/tools.d.ts.map +1 -1
- package/dist/src/types/agent.d.ts +40 -9
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +18 -8
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +15 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +5 -2
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +34 -1
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/agent.ts +72 -11
- package/src/api/server.ts +4 -2
- package/src/auto-continue.ts +4 -3
- package/src/cut-off-round.ts +98 -0
- package/src/framework.ts +2694 -700
- package/src/gate/event-gate.ts +579 -15
- package/src/gate/index.ts +8 -0
- package/src/gate/reply-judge.ts +512 -0
- package/src/gate/types.ts +164 -1
- package/src/heartbeat/heartbeat.ts +804 -0
- package/src/heartbeat/reminder-time.ts +107 -0
- package/src/index.ts +4 -0
- package/src/mcpl/capability-mask.ts +3 -2
- package/src/mcpl/channel-registry.ts +2 -1
- package/src/mcpl/inbound-source.ts +42 -69
- package/src/mcpl/server-connection.ts +56 -16
- package/src/mcpl/tags.ts +13 -2
- package/src/mcpl/tool-presence.ts +133 -67
- package/src/mcpl/tool-signature.ts +51 -20
- package/src/mcpl/toolset-host.ts +197 -107
- package/src/mcpl/toolsets-module.ts +143 -49
- package/src/mcpl/types.ts +3 -1
- package/src/module-registry.ts +8 -2
- package/src/modules/history/index.ts +22 -12
- package/src/modules/workspace/answers.ts +133 -0
- package/src/modules/workspace/branch-intent.ts +10 -5
- package/src/modules/workspace/direct-write.ts +58 -23
- package/src/modules/workspace/disk-agreement.ts +52 -21
- package/src/modules/workspace/edit-text.ts +7 -5
- package/src/modules/workspace/effects.ts +21 -12
- package/src/modules/workspace/index.ts +927 -541
- package/src/modules/workspace/observe.ts +381 -177
- package/src/modules/workspace/read-page.ts +7 -1
- package/src/modules/workspace/reconcile.ts +27 -15
- package/src/modules/workspace/result-text.ts +2 -0
- package/src/modules/workspace/sync.ts +16 -0
- package/src/modules/workspace/types.ts +96 -43
- package/src/modules/workspace/watcher.ts +8 -3
- package/src/prose-drafts.ts +4 -3
- package/src/reasoning-loss-notice.ts +62 -0
- package/src/reasoning-loss.ts +120 -0
- package/src/send-tools.ts +3 -2
- package/src/sent-rows.ts +1 -1
- package/src/thinking-drops.ts +10 -16
- package/src/tool-args.ts +34 -1
- package/src/tool-result-guard.ts +26 -11
- package/src/tune-out/tools.ts +4 -1
- package/src/types/agent.ts +40 -9
- package/src/types/events.ts +18 -8
- package/src/types/framework.ts +15 -0
- package/src/types/module.ts +5 -2
- package/src/types/trace.ts +36 -1
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a reminder's `atIso` in the agent's time zone, ported from heartbeat-mcpl
|
|
3
|
+
* (src/reminder-time.ts) when the heartbeat moved into the framework: a time
|
|
4
|
+
* without an offset is read in the zone, and a wall time a clock change skips
|
|
5
|
+
* or repeats is refused, naming the offsets that would say which instant.
|
|
6
|
+
*/
|
|
7
|
+
const DAY_MS = 86_400_000;
|
|
8
|
+
const MAX_DATE_MS = 8_640_000_000_000_000;
|
|
9
|
+
|
|
10
|
+
export const FIRST_REMINDER_TIME = '-271821-04-20T00:00:00Z';
|
|
11
|
+
export const LAST_REMINDER_TIME = '+275760-09-13T00:00:00Z';
|
|
12
|
+
|
|
13
|
+
export function canHoldTime(ms: number): boolean {
|
|
14
|
+
return Math.abs(ms) <= MAX_DATE_MS;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isCalendarDay(year: number, month: number, day: number): boolean {
|
|
18
|
+
if (month < 1 || month > 12 || day < 1) return false;
|
|
19
|
+
const leap = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
|
|
20
|
+
return day <= [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** ISO 8601 reads 24:00 as the end of the day. */
|
|
24
|
+
function isClockTime(hour: number, minute: number, second: number, fraction: number): boolean {
|
|
25
|
+
if (hour === 24) return minute === 0 && second === 0 && fraction === 0;
|
|
26
|
+
return hour <= 23 && minute <= 59 && second <= 59;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Intl names offsets with a GMT prefix, with seconds for the historical ones, and ICU 76,
|
|
30
|
+
* which Node 22.12.0 has, names the zero offset GMT alone. */
|
|
31
|
+
function offsetMilliseconds(offset: string): number | null {
|
|
32
|
+
if (/^(Z|GMT)$/i.test(offset)) return 0;
|
|
33
|
+
const match = /^(?:GMT)?([+-])(\d{2})(?:(:?)(\d{2})(?:\3(\d{2}))?)?$/.exec(offset);
|
|
34
|
+
if (!match || +match[2] > 23 || +(match[4] ?? 0) > 59 || +(match[5] ?? 0) > 59) return null;
|
|
35
|
+
return (match[1] === '-' ? -1 : 1)
|
|
36
|
+
* (+match[2] * 3600 + +(match[4] ?? 0) * 60 + +(match[5] ?? 0)) * 1000;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function parseReminderTime(atIso: string, timeZone: string): number {
|
|
40
|
+
const shown = JSON.stringify(atIso);
|
|
41
|
+
const parts = /^([+-]\d{6}|\d{4})-(\d{2})-(\d{2})(?:[Tt ](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d+))?)?([Zz]|[+-][\d:]+)?)?$/.exec(atIso);
|
|
42
|
+
// ECMAScript's date-time format refuses -000000 for year zero.
|
|
43
|
+
if (!parts || parts[1] === '-000000') {
|
|
44
|
+
throw new Error(`"atIso" ${shown} isn't a time this tool reads. Give a date and time like "2030-07-15T09:00", read in `
|
|
45
|
+
+ `${timeZone}, or end it with an offset, like "2030-07-15T09:00Z" or "2030-07-15T09:00+01:00". A date alone is `
|
|
46
|
+
+ `midnight in ${timeZone}. For a delay from now, give inSeconds, inMinutes or inHours instead.`);
|
|
47
|
+
}
|
|
48
|
+
const givenOffset = parts[8] ? offsetMilliseconds(parts[8]) : null;
|
|
49
|
+
if (parts[8] && givenOffset === null) {
|
|
50
|
+
throw new Error(`"atIso" ${shown} ends in ${JSON.stringify(parts[8])}, which isn't a UTC offset. `
|
|
51
|
+
+ 'End it with Z for UTC, or an offset like +05:30, +0530 or +05.');
|
|
52
|
+
}
|
|
53
|
+
const calendarDate = `${parts[1]}-${parts[2]}-${parts[3]}`;
|
|
54
|
+
const readIn = parts[8] ? '' : `, read in ${timeZone}`;
|
|
55
|
+
const example = (time: string) => `"${time}${parts[8] ?? ''}"${readIn}`;
|
|
56
|
+
if (!isCalendarDay(+parts[1], +parts[2], +parts[3])) {
|
|
57
|
+
throw new Error(`"atIso" ${shown} names a day the calendar doesn't have, ${calendarDate}. Give a real date, like ${example('2030-03-01T09:00')}.`);
|
|
58
|
+
}
|
|
59
|
+
// A Date keeps milliseconds, so digits past them don't count.
|
|
60
|
+
if (!isClockTime(+(parts[4] ?? 0), +(parts[5] ?? 0), +(parts[6] ?? 0), +(parts[7] ?? '0').slice(0, 3))) {
|
|
61
|
+
throw new Error(`"atIso" ${shown} has a time no clock shows: hours run 00 to 23 and minutes and seconds 00 to 59, `
|
|
62
|
+
+ `and 24:00, the end of the day, takes no minutes, seconds or milliseconds after it. Give one like ${example(`${calendarDate}T09:00`)}.`);
|
|
63
|
+
}
|
|
64
|
+
const outOfRange = new Error(`"atIso" ${shown}${readIn ? `${readIn},` : ''} is outside the times a reminder can hold, `
|
|
65
|
+
+ `${FIRST_REMINDER_TIME} to ${LAST_REMINDER_TIME}. Give a time inside that range.`);
|
|
66
|
+
// Date.parse reads 24:00 as the start of the next day.
|
|
67
|
+
const wall = Date.parse(`${calendarDate}T${parts[4] ?? '00'}:${parts[5] ?? '00'}:${parts[6] ?? '00'}.${(parts[7] ?? '0').slice(0, 3).padEnd(3, '0')}Z`);
|
|
68
|
+
if (!Number.isFinite(Date.parse(`${calendarDate}T00:00:00Z`)) || !Number.isFinite(wall)) throw outOfRange;
|
|
69
|
+
|
|
70
|
+
if (givenOffset !== null) {
|
|
71
|
+
const instant = wall - givenOffset;
|
|
72
|
+
if (Math.abs(instant) > MAX_DATE_MS) throw outOfRange;
|
|
73
|
+
return instant;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const asTime = parts[4] === undefined ? `${calendarDate}T00:00` : atIso;
|
|
77
|
+
const withOffsets = (labels: string[]) => labels.map((label) => JSON.stringify(`${asTime}${label}`)).join(' or ');
|
|
78
|
+
const formatter = new Intl.DateTimeFormat('en-US', { timeZone, timeZoneName: 'longOffset' });
|
|
79
|
+
const offsetAt = (ms: number) => {
|
|
80
|
+
const name = formatter.formatToParts(new Date(ms)).find((part) => part.type === 'timeZoneName')?.value;
|
|
81
|
+
const offset = name ? offsetMilliseconds(name) : null;
|
|
82
|
+
if (!name || offset === null) {
|
|
83
|
+
throw new Error(`"atIso" ${shown} has no offset, and the zone it would be read in, ${timeZone}, can't be read here. `
|
|
84
|
+
+ `End it with an offset, like ${withOffsets(['Z', '+01:00'])}, or give a delay from now in inSeconds, inMinutes or inHours.`);
|
|
85
|
+
}
|
|
86
|
+
return { ms: offset, label: name === 'GMT' ? '+00:00' : name.slice(3) };
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Around a clock change a wall time has no instant (a gap) or two (a fold).
|
|
90
|
+
const offsets = new Map<number, string>();
|
|
91
|
+
for (const delta of [-DAY_MS, DAY_MS]) {
|
|
92
|
+
const sample = Math.abs(wall + delta) <= MAX_DATE_MS ? wall + delta : wall;
|
|
93
|
+
const offset = offsetAt(sample);
|
|
94
|
+
offsets.set(offset.ms, offset.label);
|
|
95
|
+
}
|
|
96
|
+
const inRange = [...offsets].filter(([offset]) => Math.abs(wall - offset) <= MAX_DATE_MS);
|
|
97
|
+
if (inRange.length === 0) throw outOfRange;
|
|
98
|
+
const candidates = inRange.filter(([offset]) => offsetAt(wall - offset).ms === offset);
|
|
99
|
+
if (candidates.length === 1) return wall - candidates[0][0];
|
|
100
|
+
const read = parts[4] === undefined ? `${shown} is read as ${asTime}, which` : shown;
|
|
101
|
+
if (candidates.length > 1) {
|
|
102
|
+
throw new Error(`"atIso" ${read} happens twice in ${timeZone}, where the clocks go back over it. `
|
|
103
|
+
+ `End it with the offset you mean: ${withOffsets(candidates.map(([, label]) => label))}.`);
|
|
104
|
+
}
|
|
105
|
+
throw new Error(`"atIso" ${read} doesn't exist in ${timeZone}, where the clocks skip over it. Give a time that exists `
|
|
106
|
+
+ `there, or end it with an offset to say which instant you mean, like ${withOffsets([...offsets.values()])}.`);
|
|
107
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -70,6 +70,10 @@ export type { SessionUsage, AgentUsage, SessionUsageSnapshot, UsageUpdatedEvent
|
|
|
70
70
|
// EventGate
|
|
71
71
|
export { EventGate, wakeLineText, type WakeProvenance, type WritePlacement } from './gate/index.js';
|
|
72
72
|
export type { GateConfig, GateOptions, GatePolicy, GatePolicyMatch, GateBehavior, WakeLine } from './gate/index.js';
|
|
73
|
+
export { ReplyJudge, DEFAULT_JUDGE_MODEL, DEFAULT_JUDGE_MAX_INPUT_TOKENS, MIN_JUDGE_INPUT_TOKENS } from './gate/index.js';
|
|
74
|
+
export type { JudgeConfig, JudgeEventSummary, JudgeFn, JudgeRequest, JudgeVerdict, ReplyJudgeOptions, ReplyJudgeDeps, UpcomingWake } from './gate/index.js';
|
|
75
|
+
export { Heartbeat, DEFAULT_HEARTBEAT, HEARTBEAT_TOOL_PREFIX } from './heartbeat/heartbeat.js';
|
|
76
|
+
export type { HeartbeatOptions, HeartbeatSettings, HeartbeatWake, HeartbeatDeliveryMode, Reminder } from './heartbeat/heartbeat.js';
|
|
73
77
|
|
|
74
78
|
// MCPL channel registry (exposed for modules that need channel-level operations)
|
|
75
79
|
export { ChannelRegistry } from './mcpl/index.js';
|
|
@@ -81,8 +81,9 @@ function anyMatches(patterns: string[], path: string): boolean {
|
|
|
81
81
|
return patterns.some((pattern) => patternMatchesPath(pattern, path));
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
/** Keys that are negotiation metadata,
|
|
85
|
-
|
|
84
|
+
/** Keys that are negotiation metadata, or RFC-014 declarations (toolHints,
|
|
85
|
+
* toolsets, surface), not grantable capabilities. */
|
|
86
|
+
const UNMASKABLE_KEYS = new Set(['version', 'featureSets', 'toolHints', 'toolsets', 'surface']);
|
|
86
87
|
|
|
87
88
|
/**
|
|
88
89
|
* Interior object paths whose object value is itself the capability, with
|
|
@@ -1909,7 +1909,8 @@ export class ChannelRegistry {
|
|
|
1909
1909
|
const incomingEntry = this.channels.get(incomingKey)!;
|
|
1910
1910
|
const markAccepted = (firstView = false) => {
|
|
1911
1911
|
// Non-conversational markers (a reaction added or removed, an edit, a
|
|
1912
|
-
// deletion
|
|
1912
|
+
// deletion, and a chat:system notice such as a join or the weather)
|
|
1913
|
+
// are ABOUT a message or the place, not one the agent replies to: they
|
|
1913
1914
|
// must not retarget the default publish locus or become
|
|
1914
1915
|
// `context.incoming`'s reply anchor. An edit the coalescer delivered
|
|
1915
1916
|
// as the agent's first view counts as the message it replaced.
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import { createHash } from 'node:crypto';
|
|
25
|
+
import { blockAsStored, MalformedContentError } from '@animalabs/context-manager';
|
|
26
|
+
import { logDetail, logValue } from './log-value.js';
|
|
25
27
|
|
|
26
28
|
/** The MCPL admission lane that accepted an item. Its contract decides what
|
|
27
29
|
* an `eventId` is worth: `push/event` deduplicates by eventId, and RFC-006
|
|
@@ -192,68 +194,23 @@ export function canonicalJson(value: unknown): string {
|
|
|
192
194
|
}
|
|
193
195
|
|
|
194
196
|
/**
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
* 'base64', data, mediaType } }` and nothing else, its data re-encoded from
|
|
199
|
-
* the decoded bytes, and an image's media type taken from its bytes'
|
|
200
|
-
* signature where they have one. Any other block comes back as it was given,
|
|
201
|
-
* except a store-internal `blob_ref` from an unresolved read, which resolves
|
|
202
|
-
* to inline media by default. It is digested as given here, so an unresolved
|
|
203
|
-
* read and its resolved form differ. Context-manager refuses caller-supplied
|
|
204
|
-
* blob_ref writes; the MCPL lanes never store one.
|
|
197
|
+
* blockAsStored throws a `MalformedContentError` for a block the store
|
|
198
|
+
* refuses on write, a caller-supplied `blob_ref` among them. Such a block
|
|
199
|
+
* has no stored form, and whatever is thrown, it is digested as given.
|
|
205
200
|
*/
|
|
206
201
|
function storedBlock(block: unknown): unknown {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Base64 as the store writes it back from the decoded bytes: padded, with no
|
|
222
|
-
* whitespace, the standard alphabet and zero unused bits. Data already in
|
|
223
|
-
* that form, as every stored copy's is, is returned without decoding it all.
|
|
224
|
-
*/
|
|
225
|
-
function canonicalBase64(data: string): string {
|
|
226
|
-
if (data.length % 4 === 0 && /^[A-Za-z0-9+/]*={0,2}$/.test(data)) {
|
|
227
|
-
const last = data.slice(-4);
|
|
228
|
-
if (Buffer.from(last, 'base64').toString('base64') === last) return data;
|
|
229
|
-
}
|
|
230
|
-
return Buffer.from(data, 'base64').toString('base64');
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* The store's own test for raster image types with an unambiguous byte
|
|
235
|
-
* signature, copied from context-manager's blob-manager.ts (it isn't
|
|
236
|
-
* exported): the store relabels an image whose bytes match one of these.
|
|
237
|
-
*/
|
|
238
|
-
function sniffRasterImageMediaType(bytes: Uint8Array): string | null {
|
|
239
|
-
if (bytes.length >= 8 &&
|
|
240
|
-
bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47 &&
|
|
241
|
-
bytes[4] === 0x0d && bytes[5] === 0x0a && bytes[6] === 0x1a && bytes[7] === 0x0a) {
|
|
242
|
-
return 'image/png';
|
|
243
|
-
}
|
|
244
|
-
if (bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) {
|
|
245
|
-
return 'image/jpeg';
|
|
246
|
-
}
|
|
247
|
-
if (bytes.length >= 6) {
|
|
248
|
-
const signature = Buffer.from(bytes.subarray(0, 6)).toString('ascii');
|
|
249
|
-
if (signature === 'GIF87a' || signature === 'GIF89a') return 'image/gif';
|
|
250
|
-
}
|
|
251
|
-
if (bytes.length >= 12 &&
|
|
252
|
-
Buffer.from(bytes.subarray(0, 4)).toString('ascii') === 'RIFF' &&
|
|
253
|
-
Buffer.from(bytes.subarray(8, 12)).toString('ascii') === 'WEBP') {
|
|
254
|
-
return 'image/webp';
|
|
202
|
+
try {
|
|
203
|
+
return blockAsStored(block as Parameters<typeof blockAsStored>[0]);
|
|
204
|
+
} catch (err) {
|
|
205
|
+
if (!(err instanceof MalformedContentError)) {
|
|
206
|
+
const { name, message } = (err ?? {}) as { name?: unknown; message?: unknown };
|
|
207
|
+
console.error(
|
|
208
|
+
`[stored-digest-as-given] error=${logValue(typeof name === 'string' ? name : typeof err)}: ` +
|
|
209
|
+
logDetail(typeof message === 'string' ? message : ''),
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
return block;
|
|
255
213
|
}
|
|
256
|
-
return null;
|
|
257
214
|
}
|
|
258
215
|
|
|
259
216
|
/**
|
|
@@ -261,23 +218,39 @@ function sniffRasterImageMediaType(bytes: Uint8Array): string | null {
|
|
|
261
218
|
* room-220 #46752–#47210): SHA-256 hex of `canonicalJson([blocks])` over the
|
|
262
219
|
* ContentBlock[] the framework stores for the body — after MCPL conversion,
|
|
263
220
|
* before any host decoration is added and before storage shards it — with
|
|
264
|
-
* each block as the store keeps it
|
|
265
|
-
* hashes alike as delivered, as handed to storage, and as
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
221
|
+
* each block as the store keeps it, which context-manager's `blockAsStored`
|
|
222
|
+
* gives. So a body hashes alike as delivered, as handed to storage, and as
|
|
223
|
+
* read back unsharded from the store with its blobs resolved, except one
|
|
224
|
+
* whose blocks the store changes together: when it mends a `tool_use` whose
|
|
225
|
+
* input isn't an object, it also releases a raw replay form (`rawXml`,
|
|
226
|
+
* `rawItem`) that the message's other blocks share, which no one block's
|
|
227
|
+
* stored form shows. A body an MCPL lane stamps is never one. A block the
|
|
228
|
+
* store refuses on write, a `blob_ref` among them, is digested as given. The
|
|
229
|
+
* one-element array keeps the framing an undecorated, unsharded stored copy
|
|
230
|
+
* has always hashed to, so a stamped copy and an older copy of the same body
|
|
231
|
+
* share a version where the store keeps the body as it did then.
|
|
232
|
+
*
|
|
233
|
+
* A digest is a digest under the context-manager release that computed it: a
|
|
234
|
+
* release that changes what the store keeps, such as its set of image
|
|
235
|
+
* signatures, changes the digest of a body it touches.
|
|
269
236
|
*
|
|
270
237
|
* Ingestion stamps it as `metadata.sourceBodyDigest`, and the same function
|
|
271
238
|
* over exactly the blocks handed to storage (decorations included) as
|
|
272
239
|
* `metadata.storedBodyDigest`, on both MCPL lanes. Both live outside the
|
|
273
240
|
* frozen admission envelope: they describe the body actually delivered (a
|
|
274
241
|
* materialization, a correction), not the admission. Neither proves later
|
|
275
|
-
* presence or completeness
|
|
242
|
+
* presence or completeness. An unsharded stored copy that no longer hashes
|
|
276
243
|
* to its storedBodyDigest was changed after delivery (editMessage keeps
|
|
277
|
-
* metadata)
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
244
|
+
* metadata), or holds blocks the store changes together, or its digest was
|
|
245
|
+
* taken under a release that kept some block of it differently: a
|
|
246
|
+
* context-manager release that changed a stored form, or an agent-framework
|
|
247
|
+
* release from 0.24.0 up to the one that brought in blockAsStored, which
|
|
248
|
+
* took stored forms from a copy of the store's transform. Digests already
|
|
249
|
+
* stamped are never recomputed. A body the store shards carries the whole
|
|
250
|
+
* body's two digests on every shard, and no shard hashes to them. Check a
|
|
251
|
+
* copy read back with its blobs resolved (getAllMessages, getMessage, or
|
|
252
|
+
* getMessageWindow without `resolveBlobs: false`): a blob reference never
|
|
253
|
+
* matches.
|
|
281
254
|
*
|
|
282
255
|
* Exported from the package root, so a consumer checks a copy against either
|
|
283
256
|
* field with this same function rather than a copy of it.
|
|
@@ -240,19 +240,29 @@ export function toolsetDeclarationsOf(advertised: unknown): Record<string, unkno
|
|
|
240
240
|
/** What the RFC-014 read tap is handed as the connection reads a message
|
|
241
241
|
* that can carry hint changes (see McplServerConnection.setReadTap). */
|
|
242
242
|
export type HintRead =
|
|
243
|
-
/** A response to a request that can carry hints (RFC-014 §6.1 carrier 1)
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
|
|
243
|
+
/** A response to a request that can carry hints (RFC-014 §6.1 carrier 1),
|
|
244
|
+
* or a `tools/list`'s whole inventory (§4.1), its pages joined by
|
|
245
|
+
* sendToolsList, `straddled` when its pages carry different revs.
|
|
246
|
+
* `awaited`: whoever sent it still waits for it; false for a
|
|
247
|
+
* result that arrives after its timeout. A JSON-RPC error carries no state
|
|
248
|
+
* and isn't tapped. */
|
|
249
|
+
| { kind: 'result'; method: string; params: Record<string, unknown>; result: unknown; awaited: boolean; straddled?: boolean }
|
|
248
250
|
/** A push/event (carrier 2), as read, before the plane gate or the grant
|
|
249
251
|
* admit or refuse it. */
|
|
250
252
|
| { kind: 'push'; params: unknown }
|
|
251
253
|
/** A standalone tools/hints (carrier 3). */
|
|
252
254
|
| { kind: 'hints'; params: unknown };
|
|
253
255
|
|
|
254
|
-
/** Requests whose results can carry hint state (RFC-014 §6.1
|
|
255
|
-
const HINTED_METHODS: ReadonlySet<string> = new Set(['tools/call', 'toolsets/open', 'toolsets/close', 'toolsets/list'
|
|
256
|
+
/** Requests whose results can carry hint state, remembered for a late answer (RFC-014 §6.1). */
|
|
257
|
+
const HINTED_METHODS: ReadonlySet<string> = new Set(['tools/call', 'toolsets/open', 'toolsets/close', 'toolsets/list']);
|
|
258
|
+
|
|
259
|
+
function hintedParams(method: string, params: Record<string, unknown>): { toolset?: string; instance?: string } {
|
|
260
|
+
if (method !== 'toolsets/open' && method !== 'toolsets/close') return {};
|
|
261
|
+
return {
|
|
262
|
+
...(typeof params.toolset === 'string' ? { toolset: params.toolset } : {}),
|
|
263
|
+
...(typeof params.instance === 'string' ? { instance: params.instance } : {}),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
256
266
|
|
|
257
267
|
/**
|
|
258
268
|
* A handshake that failed, and whose cleanup couldn't confirm that the child
|
|
@@ -330,6 +340,7 @@ interface PendingRequest {
|
|
|
330
340
|
resolve: (result: unknown) => void;
|
|
331
341
|
reject: (error: Error) => void;
|
|
332
342
|
method: string;
|
|
343
|
+
onRead?: (result: unknown) => void;
|
|
333
344
|
/** Per-request timeout timer; cleared on response/close/timeout. */
|
|
334
345
|
timer?: ReturnType<typeof setTimeout>;
|
|
335
346
|
}
|
|
@@ -339,6 +350,9 @@ interface RequestOptions {
|
|
|
339
350
|
timeoutMs?: number;
|
|
340
351
|
/** Surface a late response even when it carries no checkpoint state. */
|
|
341
352
|
surfaceOrphanedResponse?: boolean;
|
|
353
|
+
/** Called with a successful result as its response is read, before
|
|
354
|
+
* anything read after it and before the request's promise settles. */
|
|
355
|
+
onRead?: (result: unknown) => void;
|
|
342
356
|
}
|
|
343
357
|
|
|
344
358
|
/** A server→host message held while its plane is closed. `kept`: it came on
|
|
@@ -452,15 +466,18 @@ export class McplServerConnection extends EventEmitter {
|
|
|
452
466
|
* the message: a push the plane holds or the grant refuses, a result its
|
|
453
467
|
* caller stopped waiting for. The tap sees every such message of the
|
|
454
468
|
* current transport, synchronously, before anything routes it; a late
|
|
455
|
-
* answer's request is remembered past its timeout (hintedRequests
|
|
456
|
-
*
|
|
469
|
+
* answer's request is remembered past its timeout (hintedRequests, while
|
|
470
|
+
* no more than 1,000 hinted requests await an answer) so its hints still
|
|
471
|
+
* apply. Null (the default) taps nothing, and the connection
|
|
457
472
|
* behaves as it always has.
|
|
458
473
|
*/
|
|
459
474
|
private readTap: ((read: HintRead, epoch: number) => void) | null = null;
|
|
460
475
|
/** The hinted requests sent on this transport, by id, kept past a timeout
|
|
461
|
-
* so a late result is still tapped
|
|
476
|
+
* so a late result is still tapped: each one's method, and an open's or
|
|
477
|
+
* close's toolset and instance, which are all the read of its result
|
|
478
|
+
* uses; never a call's arguments. Bounded. Cleared at each boundary,
|
|
462
479
|
* since the next transport numbers its requests afresh. */
|
|
463
|
-
private hintedRequests = new Map<string | number, { method: string; params:
|
|
480
|
+
private hintedRequests = new Map<string | number, { method: string; params: { toolset?: string; instance?: string } }>();
|
|
464
481
|
|
|
465
482
|
/** Install (or, with null, remove) the RFC-014 read tap (see readTap). */
|
|
466
483
|
setReadTap(tap: ((read: HintRead, epoch: number) => void) | null): void {
|
|
@@ -1708,22 +1725,40 @@ export class McplServerConnection extends EventEmitter {
|
|
|
1708
1725
|
* read it as a complete page). A page that isn't well formed (no `tools` array, a cursor
|
|
1709
1726
|
* that isn't a string), or cursors that don't terminate (a repeat, or more
|
|
1710
1727
|
* than {@link MAX_TOOL_LIST_PAGES} pages), are errors, never a partial or
|
|
1711
|
-
* empty inventory published as complete.
|
|
1728
|
+
* empty inventory published as complete. The read tap (RFC-014) is handed
|
|
1729
|
+
* the complete inventory once, with the last page's `_meta`, as the last
|
|
1730
|
+
* page's response is read, before anything read after it; a list that
|
|
1731
|
+
* fails or times out hands it nothing. It's `straddled` when its pages
|
|
1732
|
+
* carry different `mcpl/rev`s.
|
|
1712
1733
|
*/
|
|
1713
1734
|
async sendToolsList(): Promise<{ tools: McpToolDefinition[] }> {
|
|
1714
1735
|
const tools: McpToolDefinition[] = [];
|
|
1715
1736
|
const seen = new Set<string>();
|
|
1716
1737
|
let cursor: string | undefined;
|
|
1717
1738
|
let first: Record<string, unknown> | undefined;
|
|
1739
|
+
const read: McpToolDefinition[] = [];
|
|
1740
|
+
const revs = new Set<unknown>();
|
|
1741
|
+
const onRead = (result: unknown) => {
|
|
1742
|
+
if (!result || typeof result !== 'object' || !Array.isArray((result as { tools?: unknown }).tools)) return;
|
|
1743
|
+
const r = result as { tools: McpToolDefinition[]; nextCursor?: unknown; _meta?: unknown };
|
|
1744
|
+
for (const t of r.tools) read.push(t);
|
|
1745
|
+
const rev = (r._meta as Record<string, unknown> | undefined)?.['mcpl/rev'];
|
|
1746
|
+
if (typeof rev === 'number') revs.add(rev);
|
|
1747
|
+
if (r.nextCursor !== undefined && r.nextCursor !== null) return;
|
|
1748
|
+
this.tap({
|
|
1749
|
+
kind: 'result', method: 'tools/list', params: {}, ...(revs.size > 1 ? { straddled: true } : {}),
|
|
1750
|
+
result: { tools: read, ...(r._meta !== undefined ? { _meta: r._meta } : {}) }, awaited: true,
|
|
1751
|
+
});
|
|
1752
|
+
};
|
|
1718
1753
|
for (let page = 0; page < MAX_TOOL_LIST_PAGES; page++) {
|
|
1719
|
-
const result = await this.sendRequest('tools/list', cursor === undefined ? {} : { cursor }) as Record<string, unknown> | null;
|
|
1754
|
+
const result = await this.sendRequest('tools/list', cursor === undefined ? {} : { cursor }, { onRead }) as Record<string, unknown> | null;
|
|
1720
1755
|
const malformed = (why: string) =>
|
|
1721
1756
|
new Error(`MCPL server "${this.id}" sent a malformed tools/list page ${page + 1}: ${why}`);
|
|
1722
1757
|
if (!result || typeof result !== 'object' || !Array.isArray(result.tools)) {
|
|
1723
1758
|
throw malformed('no tools array');
|
|
1724
1759
|
}
|
|
1725
1760
|
first ??= result;
|
|
1726
|
-
|
|
1761
|
+
for (const t of result.tools as McpToolDefinition[]) tools.push(t);
|
|
1727
1762
|
if (result.nextCursor === undefined || result.nextCursor === null) {
|
|
1728
1763
|
// The first page's other fields are kept as they came, so a
|
|
1729
1764
|
// single-page answer is returned exactly as before.
|
|
@@ -2176,7 +2211,7 @@ export class McplServerConnection extends EventEmitter {
|
|
|
2176
2211
|
}
|
|
2177
2212
|
|
|
2178
2213
|
return new Promise((resolve, reject) => {
|
|
2179
|
-
const pending: PendingRequest = { resolve, reject, method };
|
|
2214
|
+
const pending: PendingRequest = { resolve, reject, method, ...(options.onRead ? { onRead: options.onRead } : {}) };
|
|
2180
2215
|
const timeoutMs = options.timeoutMs ?? this.requestTimeoutMs;
|
|
2181
2216
|
if (timeoutMs > 0) {
|
|
2182
2217
|
pending.timer = setTimeout(() => {
|
|
@@ -2218,7 +2253,7 @@ export class McplServerConnection extends EventEmitter {
|
|
|
2218
2253
|
// RFC-014: a request whose result can carry hints is remembered past
|
|
2219
2254
|
// its timeout, so a late result's hints still apply (readTap).
|
|
2220
2255
|
if (this.readTap && HINTED_METHODS.has(method)) {
|
|
2221
|
-
this.hintedRequests.set(id, { method, params });
|
|
2256
|
+
this.hintedRequests.set(id, { method, params: hintedParams(method, params) });
|
|
2222
2257
|
if (this.hintedRequests.size > 1000) this.hintedRequests.delete(this.hintedRequests.keys().next().value!);
|
|
2223
2258
|
}
|
|
2224
2259
|
try {
|
|
@@ -2654,6 +2689,11 @@ export class McplServerConnection extends EventEmitter {
|
|
|
2654
2689
|
logValueRedacted(response.error.message, redact),
|
|
2655
2690
|
);
|
|
2656
2691
|
} else {
|
|
2692
|
+
try {
|
|
2693
|
+
pending.onRead?.(response.result);
|
|
2694
|
+
} catch (error) {
|
|
2695
|
+
console.error(`MCPL server "${this.id}": reading ${pending.method}'s result failed: ${logDetail(describeCause(error))}`);
|
|
2696
|
+
}
|
|
2657
2697
|
pending.resolve(response.result);
|
|
2658
2698
|
}
|
|
2659
2699
|
}
|
package/src/mcpl/tags.ts
CHANGED
|
@@ -21,8 +21,10 @@ const CORE_IMPLIES: Readonly<Record<string, readonly string[]>> = {
|
|
|
21
21
|
/**
|
|
22
22
|
* RFC-001 core tags that mark a message as being ABOUT the conversation
|
|
23
23
|
* rather than part of it: reactions (added or removed), edits and deletions
|
|
24
|
-
* of an existing message
|
|
25
|
-
*
|
|
24
|
+
* of an existing message, and (revision 3) `chat:system` notices from the
|
|
25
|
+
* platform or the place: a join, a pin, the weather changing in a world.
|
|
26
|
+
* These are markers, not turns — they must not clear explicit-send
|
|
27
|
+
* suppression, count as a new conversational round, or
|
|
26
28
|
* retarget the default publish channel. An edit can also be the first
|
|
27
29
|
* version of a message the agent sees, and then counts as that message:
|
|
28
30
|
* isMarkerTagged reads the host's mark for the case the coalescer knows
|
|
@@ -36,6 +38,7 @@ export const NON_CONVERSATIONAL_TAGS: ReadonlySet<string> = new Set([
|
|
|
36
38
|
'chat:reaction-remove',
|
|
37
39
|
'chat:edited',
|
|
38
40
|
'chat:deleted',
|
|
41
|
+
'chat:system',
|
|
39
42
|
]);
|
|
40
43
|
|
|
41
44
|
/** True when a (possibly untyped) tag list carries a non-conversational marker tag. */
|
|
@@ -54,11 +57,19 @@ export function isMarkerTagged(tags: unknown, firstView: boolean): boolean {
|
|
|
54
57
|
return hasNonConversationalTag(firstView && Array.isArray(tags) ? tags.filter((t) => t !== 'chat:edited') : tags);
|
|
55
58
|
}
|
|
56
59
|
|
|
60
|
+
/** True when a (possibly untyped) tag list marks a `chat:system` notice: an
|
|
61
|
+
* event about the place, not a message anyone sent (RFC-001 rev 3). */
|
|
62
|
+
export function isSystemNotice(tags: unknown): boolean {
|
|
63
|
+
return Array.isArray(tags) && tags.includes('chat:system');
|
|
64
|
+
}
|
|
65
|
+
|
|
57
66
|
/**
|
|
58
67
|
* Expand a raw tag list over the normative core closure, then resolve the
|
|
59
68
|
* §16.2 mutual exclusion: `chat:addressed` and `chat:ambient` cannot both
|
|
60
69
|
* stand after expansion, and addressed wins — a message that is both
|
|
61
70
|
* specifically-to-you and background noise is specifically-to-you.
|
|
71
|
+
* `chat:system` is another facet (kind, not addressing) and stands beside
|
|
72
|
+
* either: a pin in a DM is addressed by its place and still a notice.
|
|
62
73
|
*
|
|
63
74
|
* Pure; input order is preserved for the tags that survive, with implied
|
|
64
75
|
* tags appended in first-cause order. Unknown and producer-namespaced tags
|