@getfamiliar/shared 0.1.2
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/build/AgentRun.d.ts +187 -0
- package/build/AgentRun.js +1 -0
- package/build/AgentRunBus.d.ts +147 -0
- package/build/AgentRunBus.js +362 -0
- package/build/Calendar.d.ts +450 -0
- package/build/Calendar.js +37 -0
- package/build/ChatMessage.d.ts +53 -0
- package/build/ChatMessage.js +11 -0
- package/build/ChatMessageBus.d.ts +110 -0
- package/build/ChatMessageBus.js +257 -0
- package/build/Config.d.ts +83 -0
- package/build/Config.js +1 -0
- package/build/ContainerTool.d.ts +34 -0
- package/build/ContainerTool.js +1 -0
- package/build/CronExpression.d.ts +53 -0
- package/build/CronExpression.js +77 -0
- package/build/Event.d.ts +280 -0
- package/build/Event.js +14 -0
- package/build/EventBus.d.ts +198 -0
- package/build/EventBus.js +482 -0
- package/build/HandlerCatalog.d.ts +75 -0
- package/build/HandlerCatalog.js +165 -0
- package/build/InferenceEvent.d.ts +30 -0
- package/build/InferenceEvent.js +1 -0
- package/build/InferenceEventBus.d.ts +39 -0
- package/build/InferenceEventBus.js +80 -0
- package/build/Mail.d.ts +250 -0
- package/build/Mail.js +73 -0
- package/build/MailStyleTemplate.d.ts +71 -0
- package/build/MailStyleTemplate.js +47 -0
- package/build/ModelMetaData.d.ts +63 -0
- package/build/ModelMetaData.js +12 -0
- package/build/PathGlob.d.ts +20 -0
- package/build/PathGlob.js +33 -0
- package/build/Plugin.d.ts +709 -0
- package/build/Plugin.js +29 -0
- package/build/PostgresConnection.d.ts +75 -0
- package/build/PostgresConnection.js +162 -0
- package/build/ScheduledSubagent.d.ts +49 -0
- package/build/ScheduledSubagent.js +1 -0
- package/build/ScheduledSubagentBus.d.ts +94 -0
- package/build/ScheduledSubagentBus.js +187 -0
- package/build/Schema.d.ts +84 -0
- package/build/Schema.js +570 -0
- package/build/StepResult.d.ts +103 -0
- package/build/StepResult.js +1 -0
- package/build/StepResultBus.d.ts +64 -0
- package/build/StepResultBus.js +169 -0
- package/build/Timezone.d.ts +58 -0
- package/build/Timezone.js +84 -0
- package/build/ToolCall.d.ts +25 -0
- package/build/ToolCall.js +1 -0
- package/build/ToolCallBus.d.ts +65 -0
- package/build/ToolCallBus.js +110 -0
- package/build/ToolLevel.d.ts +20 -0
- package/build/ToolLevel.js +2 -0
- package/build/ToolRunner.d.ts +118 -0
- package/build/ToolRunner.js +192 -0
- package/build/ToolsExpressionUtils.d.ts +80 -0
- package/build/ToolsExpressionUtils.js +102 -0
- package/build/WorkspaceFile.d.ts +104 -0
- package/build/WorkspaceFile.js +7 -0
- package/build/estimateTokens.d.ts +16 -0
- package/build/estimateTokens.js +18 -0
- package/build/index.d.ts +37 -0
- package/build/index.js +26 -0
- package/build/logging/Logger.d.ts +66 -0
- package/build/logging/Logger.js +90 -0
- package/build/markdownHtml.d.ts +14 -0
- package/build/markdownHtml.js +21 -0
- package/build/markdownTerminal.d.ts +14 -0
- package/build/markdownTerminal.js +376 -0
- package/build/testing/MockAgentRunBus.d.ts +52 -0
- package/build/testing/MockAgentRunBus.js +175 -0
- package/build/testing/MockBusStore.d.ts +22 -0
- package/build/testing/MockBusStore.js +22 -0
- package/build/testing/MockEventBus.d.ts +20 -0
- package/build/testing/MockEventBus.js +57 -0
- package/build/testing/MockLogger.d.ts +52 -0
- package/build/testing/MockLogger.js +65 -0
- package/build/testing/index.d.ts +4 -0
- package/build/testing/index.js +4 -0
- package/package.json +63 -0
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core calendar data model. Lives in `shared/` because both the host
|
|
3
|
+
* (which owns persistence and the `cal_*` agent tools) and the plugin
|
|
4
|
+
* providers (which feed the cache and implement create / attach
|
|
5
|
+
* callbacks) trade in the same row shapes. No persistence code here —
|
|
6
|
+
* the host's `CalendarStore` is the only writer.
|
|
7
|
+
*
|
|
8
|
+
* Two layers:
|
|
9
|
+
*
|
|
10
|
+
* 1. **`CalendarRow`** — one logical calendar, owned by one provider
|
|
11
|
+
* plugin (`pluginId`). Identified uniquely by `(pluginId, uniqueKey)`.
|
|
12
|
+
* 2. **`CalendarEventRow`** — one instance of an appointment. PK is the
|
|
13
|
+
* provider-prefixed event id (e.g. `ms365:AAMkAGI...`), so multiple
|
|
14
|
+
* providers can coexist in the same table without collision.
|
|
15
|
+
*
|
|
16
|
+
* Plugins persist *every* occurrence of a recurring series — no
|
|
17
|
+
* master-expansion happens in the core. The `seriesMasterId` /
|
|
18
|
+
* `type` fields let callers reconstruct the series shape when they
|
|
19
|
+
* need to.
|
|
20
|
+
*/
|
|
21
|
+
export type CalendarType = "own" | "shared";
|
|
22
|
+
export interface CalendarRow {
|
|
23
|
+
/** Numeric primary key (`bigserial`), returned as string. */
|
|
24
|
+
readonly id: string;
|
|
25
|
+
/** Plugin id that owns this calendar (e.g. `"ms365"`). */
|
|
26
|
+
readonly pluginId: string;
|
|
27
|
+
/** Provider-specific stable id (e.g. Graph calendar id). */
|
|
28
|
+
readonly uniqueKey: string;
|
|
29
|
+
/** Human-friendly display name. May collide across plugins. */
|
|
30
|
+
readonly name: string;
|
|
31
|
+
/** Whether the signed-in user owns this calendar or it was shared. */
|
|
32
|
+
readonly type: CalendarType;
|
|
33
|
+
/** Display name of the original owner for shared calendars. */
|
|
34
|
+
readonly ownerName: string | null;
|
|
35
|
+
/**
|
|
36
|
+
* The provider's own "default calendar" flag. Used by
|
|
37
|
+
* `resolveDefaultCalendar` as the fallback when
|
|
38
|
+
* `core.defaultCalendar` is unset.
|
|
39
|
+
*/
|
|
40
|
+
readonly isDefault: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Monotonically-increasing tag bumped on each full refresh walk.
|
|
43
|
+
* Rows still tagged with an old generation after a refresh
|
|
44
|
+
* completes get DELETEd — that is how the core reconciles
|
|
45
|
+
* deletions when no per-event tombstone arrives.
|
|
46
|
+
*/
|
|
47
|
+
readonly scanGeneration: number;
|
|
48
|
+
readonly createdAt: Date;
|
|
49
|
+
}
|
|
50
|
+
export interface UpsertCalendarInput {
|
|
51
|
+
readonly pluginId: string;
|
|
52
|
+
readonly uniqueKey: string;
|
|
53
|
+
readonly name: string;
|
|
54
|
+
readonly type: CalendarType;
|
|
55
|
+
readonly ownerName?: string | null;
|
|
56
|
+
readonly isDefault?: boolean;
|
|
57
|
+
}
|
|
58
|
+
export type CalendarEventType = "singleInstance" | "occurrence" | "exception" | "seriesMaster";
|
|
59
|
+
export type CalendarShowAs = "busy" | "free" | "tentative" | "oof" | "workingElsewhere";
|
|
60
|
+
export type CalendarSensitivity = "normal" | "personal" | "private" | "confidential";
|
|
61
|
+
export type CalendarImportance = "low" | "normal" | "high";
|
|
62
|
+
export type CalendarResponseStatus = "none" | "accepted" | "tentative" | "declined" | "organizer";
|
|
63
|
+
/**
|
|
64
|
+
* One attendee on a calendar event. Mirrors the Graph shape but is
|
|
65
|
+
* shared between providers; the `response` field stays optional because
|
|
66
|
+
* not every provider distinguishes status types.
|
|
67
|
+
*/
|
|
68
|
+
export interface CalendarAttendee {
|
|
69
|
+
readonly name: string | null;
|
|
70
|
+
readonly email: string;
|
|
71
|
+
readonly type: "required" | "optional" | "resource" | null;
|
|
72
|
+
readonly response: CalendarResponseStatus | null;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Lightweight attachment metadata stored on the event row. Actual
|
|
76
|
+
* bytes are fetched on demand via {@link CalendarProvider.getAttachments}.
|
|
77
|
+
*/
|
|
78
|
+
export interface CalendarAttachmentMeta {
|
|
79
|
+
readonly id: string;
|
|
80
|
+
readonly name: string;
|
|
81
|
+
readonly contentType: string | null;
|
|
82
|
+
readonly size: number | null;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Persisted shape of a calendar event row. **Internal storage truth;
|
|
86
|
+
* the agent never sees this directly.** Read tools (`cal_get_events`,
|
|
87
|
+
* `cal_get_event`) and `calendar:{new,update,delete}` bus payloads
|
|
88
|
+
* project through `renderEventForAgent` in the host first — the agent
|
|
89
|
+
* sees an `AgentEventView` whose `start` / `end` are wall-clock strings
|
|
90
|
+
* in `core.timezone`. Storage stays canonical UTC.
|
|
91
|
+
*/
|
|
92
|
+
export interface CalendarEventRow {
|
|
93
|
+
/**
|
|
94
|
+
* Provider-prefixed id (`<pluginId>:<provider-real-id>`, e.g.
|
|
95
|
+
* `ms365:AAMkAGI...`). Built via {@link buildCalendarEventId};
|
|
96
|
+
* parsed back into its two segments by {@link parseCalendarEventId}.
|
|
97
|
+
* Stable across polls.
|
|
98
|
+
*/
|
|
99
|
+
readonly id: string;
|
|
100
|
+
/** FK to `calendars.id`. */
|
|
101
|
+
readonly calendarId: string;
|
|
102
|
+
/** When this row instances a recurring series — the master's id. */
|
|
103
|
+
readonly seriesMasterId: string | null;
|
|
104
|
+
readonly type: CalendarEventType;
|
|
105
|
+
readonly subject: string | null;
|
|
106
|
+
/** UTC ISO-8601 with trailing `Z`. Canonical storage form. */
|
|
107
|
+
readonly startDt: string;
|
|
108
|
+
/** UTC ISO-8601 with trailing `Z`. Canonical storage form. */
|
|
109
|
+
readonly endDt: string;
|
|
110
|
+
/**
|
|
111
|
+
* Original IANA timezone the event was authored in (e.g. the user
|
|
112
|
+
* typed "8am Berlin"). Carried separately from `startDt`/`endDt`
|
|
113
|
+
* for round-tripping writes back to providers; not consulted by
|
|
114
|
+
* the agent-facing renderer, which always projects into
|
|
115
|
+
* `core.timezone`.
|
|
116
|
+
*/
|
|
117
|
+
readonly eventTz: string | null;
|
|
118
|
+
readonly isAllDay: boolean;
|
|
119
|
+
readonly isCancelled: boolean;
|
|
120
|
+
readonly showAs: CalendarShowAs | null;
|
|
121
|
+
readonly sensitivity: CalendarSensitivity | null;
|
|
122
|
+
readonly importance: CalendarImportance | null;
|
|
123
|
+
readonly location: string | null;
|
|
124
|
+
readonly isOnlineMeeting: boolean;
|
|
125
|
+
readonly onlineMeetingUrl: string | null;
|
|
126
|
+
readonly organizerName: string | null;
|
|
127
|
+
readonly organizerEmail: string | null;
|
|
128
|
+
readonly responseStatus: CalendarResponseStatus | null;
|
|
129
|
+
readonly attendees: readonly CalendarAttendee[] | null;
|
|
130
|
+
readonly body: string | null;
|
|
131
|
+
readonly attachments: readonly CalendarAttachmentMeta[] | null;
|
|
132
|
+
readonly scanGeneration: number;
|
|
133
|
+
readonly createdAt: Date;
|
|
134
|
+
readonly updatedAt: Date;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Input shape for {@link CalendarApi.addEvent}. The host sets
|
|
138
|
+
* `scanGeneration` from the active refresh / poll generation; callers
|
|
139
|
+
* only provide the domain fields.
|
|
140
|
+
*/
|
|
141
|
+
export interface NewCalendarEvent {
|
|
142
|
+
readonly id: string;
|
|
143
|
+
readonly calendarId: string;
|
|
144
|
+
readonly seriesMasterId?: string | null;
|
|
145
|
+
readonly type: CalendarEventType;
|
|
146
|
+
readonly subject: string | null;
|
|
147
|
+
readonly startDt: string;
|
|
148
|
+
readonly endDt: string;
|
|
149
|
+
readonly eventTz?: string | null;
|
|
150
|
+
readonly isAllDay?: boolean;
|
|
151
|
+
readonly isCancelled?: boolean;
|
|
152
|
+
readonly showAs?: CalendarShowAs | null;
|
|
153
|
+
readonly sensitivity?: CalendarSensitivity | null;
|
|
154
|
+
readonly importance?: CalendarImportance | null;
|
|
155
|
+
readonly location?: string | null;
|
|
156
|
+
readonly isOnlineMeeting?: boolean;
|
|
157
|
+
readonly onlineMeetingUrl?: string | null;
|
|
158
|
+
readonly organizerName?: string | null;
|
|
159
|
+
readonly organizerEmail?: string | null;
|
|
160
|
+
readonly responseStatus?: CalendarResponseStatus | null;
|
|
161
|
+
readonly attendees?: readonly CalendarAttendee[] | null;
|
|
162
|
+
readonly body?: string | null;
|
|
163
|
+
readonly attachments?: readonly CalendarAttachmentMeta[] | null;
|
|
164
|
+
readonly scanGeneration: number;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Standardized payload of a `calendar:{new,update,delete}:<pluginId>`
|
|
168
|
+
* bus event. Every calendar plugin emits this same shape via
|
|
169
|
+
* `CalendarService.emitFromRow` so downstream handlers don't need to
|
|
170
|
+
* know which provider produced the change.
|
|
171
|
+
*
|
|
172
|
+
* **Time fields are rendered in `core.timezone`** — `start` and `end`
|
|
173
|
+
* are wall-clock-plus-offset strings (e.g. `2026-05-21T08:00:00+02:00`),
|
|
174
|
+
* matching what the agent sees on `cal_get_events` / `cal_get_event`.
|
|
175
|
+
* `eventTz` carries the **original** IANA zone the event was authored
|
|
176
|
+
* in, for handlers that care about cross-zone reasoning.
|
|
177
|
+
*
|
|
178
|
+
* For `delete`, the fields reflect the row's last-known state captured
|
|
179
|
+
* before {@link CalendarApi.removeEvent} ran (the helper must be called
|
|
180
|
+
* BEFORE the removal — otherwise the lookup fails).
|
|
181
|
+
*/
|
|
182
|
+
export interface CalendarChangePayload {
|
|
183
|
+
readonly verb: "new" | "update" | "delete";
|
|
184
|
+
readonly pluginId: string;
|
|
185
|
+
readonly calendarEventId: string;
|
|
186
|
+
readonly calendarId: string;
|
|
187
|
+
readonly subject: string | null;
|
|
188
|
+
/** Wall-clock ISO with offset in `core.timezone`. */
|
|
189
|
+
readonly start: string;
|
|
190
|
+
/** Wall-clock ISO with offset in `core.timezone`. */
|
|
191
|
+
readonly end: string;
|
|
192
|
+
/** Original IANA zone the event was authored in; informational only. */
|
|
193
|
+
readonly eventTz: string | null;
|
|
194
|
+
readonly isAllDay: boolean;
|
|
195
|
+
readonly isCancelled: boolean;
|
|
196
|
+
readonly organizerEmail: string | null;
|
|
197
|
+
readonly location: string | null;
|
|
198
|
+
/** ISO-8601 string of the row's `updatedAt` at emit time. */
|
|
199
|
+
readonly updatedAt: string;
|
|
200
|
+
}
|
|
201
|
+
export interface FindEventsQuery {
|
|
202
|
+
/** Numeric id of the calendar — usually resolved via `resolveCalendarRef`. */
|
|
203
|
+
readonly calendarId?: string;
|
|
204
|
+
/** Inclusive ISO-8601 lower bound on `startDt`. */
|
|
205
|
+
readonly from?: string;
|
|
206
|
+
/** Exclusive ISO-8601 upper bound on `endDt`. */
|
|
207
|
+
readonly to?: string;
|
|
208
|
+
/** Case-insensitive substring filter on subject / body. */
|
|
209
|
+
readonly text?: string;
|
|
210
|
+
/** Excludes `seriesMaster` rows from results by default. */
|
|
211
|
+
readonly includeMasters?: boolean;
|
|
212
|
+
/** Excludes cancelled rows from results by default. */
|
|
213
|
+
readonly includeCancelled?: boolean;
|
|
214
|
+
readonly limit?: number;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Plugin-agnostic update-event patch. Every field is optional — only
|
|
218
|
+
* the ones present in the patch are sent through to the provider.
|
|
219
|
+
* Mirrors {@link CreateEventInput} so the agent learns one schema
|
|
220
|
+
* for both surfaces.
|
|
221
|
+
*
|
|
222
|
+
* Notable omission: `isVideocall`. Toggling online-meeting state on
|
|
223
|
+
* an existing event has provider-specific gotchas (Graph mints a new
|
|
224
|
+
* Teams join URL; some providers refuse the patch); v1 callers
|
|
225
|
+
* recreate the event instead.
|
|
226
|
+
*/
|
|
227
|
+
export interface UpdateEventInput {
|
|
228
|
+
readonly subject?: string;
|
|
229
|
+
readonly start?: string;
|
|
230
|
+
readonly end?: string;
|
|
231
|
+
readonly timezone?: string;
|
|
232
|
+
readonly body?: string;
|
|
233
|
+
readonly location?: string;
|
|
234
|
+
readonly attendees?: readonly {
|
|
235
|
+
email: string;
|
|
236
|
+
name?: string;
|
|
237
|
+
}[];
|
|
238
|
+
readonly showAs?: CalendarShowAs;
|
|
239
|
+
readonly sensitivity?: CalendarSensitivity;
|
|
240
|
+
readonly reminderMinutesBeforeStart?: number;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Plugin-agnostic create-event input. Translated to provider-specific
|
|
244
|
+
* payloads by each `CalendarProvider.createEvent` impl.
|
|
245
|
+
*
|
|
246
|
+
* `body` is markdown; providers render to HTML where applicable.
|
|
247
|
+
* Attendees may be bare addresses (`"a@example.com"`) or
|
|
248
|
+
* `{email, name?}` objects — the helper `normalizeAttendeesInput` in
|
|
249
|
+
* the core tool layer normalises both shapes.
|
|
250
|
+
*/
|
|
251
|
+
export interface CreateEventInput {
|
|
252
|
+
readonly subject: string;
|
|
253
|
+
readonly start: string;
|
|
254
|
+
readonly end: string;
|
|
255
|
+
readonly body?: string;
|
|
256
|
+
readonly location?: string;
|
|
257
|
+
readonly attendees?: readonly {
|
|
258
|
+
email: string;
|
|
259
|
+
name?: string;
|
|
260
|
+
}[];
|
|
261
|
+
readonly showAs?: CalendarShowAs;
|
|
262
|
+
readonly sensitivity?: CalendarSensitivity;
|
|
263
|
+
readonly reminderMinutesBeforeStart?: number;
|
|
264
|
+
readonly attachments?: readonly {
|
|
265
|
+
name: string;
|
|
266
|
+
contents: Buffer;
|
|
267
|
+
}[];
|
|
268
|
+
readonly isVideocall?: boolean;
|
|
269
|
+
/** IANA tz hint. Falls back to `core.timezone` when omitted. */
|
|
270
|
+
readonly timezone?: string;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* The contract a provider plugin implements + registers via
|
|
274
|
+
* {@link CalendarApi.registerProvider}. The core dispatches
|
|
275
|
+
* write tools (`cal_create_event`, `cal_attach_file`,
|
|
276
|
+
* `cal_get_event_attachments`) to the provider owning the target
|
|
277
|
+
* calendar.
|
|
278
|
+
*
|
|
279
|
+
* Every `eventId` argument is a **bare provider-native id** (no
|
|
280
|
+
* `<pluginId>:` prefix). The core strips the prefix at the dispatch
|
|
281
|
+
* boundary using {@link parseCalendarEventId} so each provider sees
|
|
282
|
+
* only the id format its upstream API expects. Provider implementations
|
|
283
|
+
* return bare ids from {@link CalendarEventRow} fields too — the
|
|
284
|
+
* mapping helpers wrap them with {@link buildCalendarEventId} before
|
|
285
|
+
* persistence so every id the agent sees is self-routing.
|
|
286
|
+
*/
|
|
287
|
+
export interface CalendarProvider {
|
|
288
|
+
readonly pluginId: string;
|
|
289
|
+
/**
|
|
290
|
+
* Create a fresh event on the given calendar and return the
|
|
291
|
+
* persisted row shape. The core inserts the returned row into
|
|
292
|
+
* the cache. Emission of `calendar:new:<pluginId>` is the
|
|
293
|
+
* caller's responsibility (via {@link CalendarApi.emitCalendarEvent}) —
|
|
294
|
+
* agent-driven creates intentionally do not emit so the agent
|
|
295
|
+
* doesn't re-trigger itself.
|
|
296
|
+
*/
|
|
297
|
+
createEvent(calendar: CalendarRow, input: CreateEventInput): Promise<CalendarEventRow>;
|
|
298
|
+
/**
|
|
299
|
+
* Apply a partial update to an existing event. `eventId` is the
|
|
300
|
+
* bare provider id. Returns the post-patch row in the same shape
|
|
301
|
+
* `createEvent` does — the core upserts it into the cache. No bus
|
|
302
|
+
* emission happens here; see {@link CalendarApi.emitCalendarEvent}.
|
|
303
|
+
*/
|
|
304
|
+
updateEvent(calendar: CalendarRow, eventId: string, patch: UpdateEventInput): Promise<CalendarEventRow>;
|
|
305
|
+
/**
|
|
306
|
+
* Delete an event at the provider. `eventId` is the bare provider
|
|
307
|
+
* id. The core removes the row from the local cache only after
|
|
308
|
+
* this resolves so a provider error leaves the cache unchanged.
|
|
309
|
+
*/
|
|
310
|
+
deleteEvent(calendar: CalendarRow, eventId: string): Promise<void>;
|
|
311
|
+
/**
|
|
312
|
+
* Inline-attach a file (≤3MB) onto an existing event. `eventId`
|
|
313
|
+
* is the bare provider id.
|
|
314
|
+
*/
|
|
315
|
+
attachFile(calendar: CalendarRow, eventId: string, name: string, contents: Buffer): Promise<void>;
|
|
316
|
+
/**
|
|
317
|
+
* Fetch every non-inline attachment for an event as in-memory
|
|
318
|
+
* buffers. `eventId` is the bare provider id. The core tool stages
|
|
319
|
+
* these into the agentrun's scratch dir.
|
|
320
|
+
*/
|
|
321
|
+
getAttachments(calendar: CalendarRow, eventId: string): Promise<readonly {
|
|
322
|
+
name: string;
|
|
323
|
+
contents: Buffer;
|
|
324
|
+
}[]>;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Capabilities the host exposes to plugins for calendar data. Reached
|
|
328
|
+
* via `ctx.calendar` on a plugin's `HostContext`. Providers call
|
|
329
|
+
* `registerProvider` once during `start()`; pollers call `upsertCalendar`
|
|
330
|
+
* and the refresh / addEvent / removeEvent helpers; agent-facing tools
|
|
331
|
+
* (host-owned) use the read side (`findEvents`, `getEvent`,
|
|
332
|
+
* `resolveDefaultCalendar`, `resolveCalendarRef`).
|
|
333
|
+
*/
|
|
334
|
+
export interface CalendarApi {
|
|
335
|
+
/**
|
|
336
|
+
* Register the create / attach callbacks a provider plugin
|
|
337
|
+
* implements. Throws if the same `pluginId` registers twice —
|
|
338
|
+
* a wiring bug, not a feature.
|
|
339
|
+
*/
|
|
340
|
+
registerProvider(provider: CalendarProvider): void;
|
|
341
|
+
upsertCalendar(input: UpsertCalendarInput): Promise<CalendarRow>;
|
|
342
|
+
/**
|
|
343
|
+
* Bump the calendar's `scanGeneration` and return the new value.
|
|
344
|
+
* The caller then upserts every event in the lookback..lookahead
|
|
345
|
+
* window tagged with this generation; once the walk completes,
|
|
346
|
+
* {@link endRefresh} deletes rows still on prior generations.
|
|
347
|
+
*/
|
|
348
|
+
beginRefresh(calendarId: string): Promise<number>;
|
|
349
|
+
/**
|
|
350
|
+
* Finalise a refresh walk: DELETE every event for `calendarId`
|
|
351
|
+
* whose `scanGeneration < gen`. Returns the count removed and the
|
|
352
|
+
* removed row data. The core emits one
|
|
353
|
+
* `calendar:delete:<pluginId>` per returned row so handlers see
|
|
354
|
+
* refresh-time deletions the same way they see tombstone-driven
|
|
355
|
+
* deletions from the incremental poll.
|
|
356
|
+
*/
|
|
357
|
+
endRefresh(calendarId: string, gen: number): Promise<{
|
|
358
|
+
removed: number;
|
|
359
|
+
rows: readonly CalendarEventRow[];
|
|
360
|
+
}>;
|
|
361
|
+
/**
|
|
362
|
+
* Upsert one event into the cache. Returns `{created, changed}`:
|
|
363
|
+
* - `created: true` when the row was a fresh INSERT (drives
|
|
364
|
+
* `calendar:new`).
|
|
365
|
+
* - `changed: true` when at least one substantive field differs
|
|
366
|
+
* from what was previously persisted, OR when the row is new.
|
|
367
|
+
* Pollers should gate `calendar:update` on this flag so a
|
|
368
|
+
* stale-cursor replay (Graph re-reports events whose
|
|
369
|
+
* `lastModifiedDateTime` bumped for non-user reasons) does not
|
|
370
|
+
* surface as a flood of phantom updates.
|
|
371
|
+
*
|
|
372
|
+
* Does NOT emit any bus event — the caller (a plugin poller)
|
|
373
|
+
* decides whether to call {@link emitCalendarEvent} based on these
|
|
374
|
+
* flags and whether the call site represents a refresh re-walk or
|
|
375
|
+
* a real change. `opts.seed` is retained as a no-op for forward
|
|
376
|
+
* compatibility with future emission policies.
|
|
377
|
+
*/
|
|
378
|
+
addEvent(row: NewCalendarEvent, opts: {
|
|
379
|
+
seed: boolean;
|
|
380
|
+
}): Promise<{
|
|
381
|
+
created: boolean;
|
|
382
|
+
changed: boolean;
|
|
383
|
+
}>;
|
|
384
|
+
removeEvent(id: string): Promise<void>;
|
|
385
|
+
/**
|
|
386
|
+
* Emit a standardized calendar-change bus event for `eventId`.
|
|
387
|
+
* `topic` must be `calendar:new:<pluginId>`,
|
|
388
|
+
* `calendar:update:<pluginId>`, or `calendar:delete:<pluginId>`,
|
|
389
|
+
* where `<pluginId>` matches the pluginId of the calendar the
|
|
390
|
+
* event belongs to. The helper fetches the row, constructs the
|
|
391
|
+
* shared {@link CalendarChangePayload}, and posts to the bus.
|
|
392
|
+
*
|
|
393
|
+
* Delete contract: callers MUST call this BEFORE
|
|
394
|
+
* {@link removeEvent}, otherwise the row lookup returns null and
|
|
395
|
+
* the helper throws.
|
|
396
|
+
*
|
|
397
|
+
* Refresh contract: pollers should suppress `calendar:update`
|
|
398
|
+
* emissions during a full re-walk so re-saving an unchanged row
|
|
399
|
+
* doesn't carpet-bomb handlers. New events found during a refresh
|
|
400
|
+
* (i.e. `addEvent` returned `{created: true}`) should still emit
|
|
401
|
+
* `calendar:new` — they are genuinely new.
|
|
402
|
+
*
|
|
403
|
+
* Throws on malformed topic, unknown row, or `pluginId` mismatch
|
|
404
|
+
* between the topic suffix and the calendar's owner.
|
|
405
|
+
*/
|
|
406
|
+
emitCalendarEvent(topic: string, eventId: string): Promise<void>;
|
|
407
|
+
findEvents(q: FindEventsQuery): Promise<readonly CalendarEventRow[]>;
|
|
408
|
+
getEvent(id: string): Promise<CalendarEventRow | null>;
|
|
409
|
+
/** Enumerate every registered calendar, optionally filtered by plugin. */
|
|
410
|
+
listCalendars(filter?: {
|
|
411
|
+
pluginId?: string;
|
|
412
|
+
}): Promise<readonly CalendarRow[]>;
|
|
413
|
+
/**
|
|
414
|
+
* Parse `core.defaultCalendar` and resolve it to a row. Returns
|
|
415
|
+
* `null` when the setting is unset OR the named calendar hasn't
|
|
416
|
+
* been discovered yet by its provider (startup race).
|
|
417
|
+
*/
|
|
418
|
+
resolveDefaultCalendar(): Promise<CalendarRow | null>;
|
|
419
|
+
/**
|
|
420
|
+
* Parse a calendar reference and return the matching row.
|
|
421
|
+
* Accepted formats:
|
|
422
|
+
* - `"<name>"` — matches by name across every plugin; throws if
|
|
423
|
+
* ambiguous (two providers share the name).
|
|
424
|
+
* - `"<pluginId>:<name>"` — name match restricted to that plugin.
|
|
425
|
+
* Returns `null` when no match exists.
|
|
426
|
+
*/
|
|
427
|
+
resolveCalendarRef(ref: string): Promise<CalendarRow | null>;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Construct a calendar event id in the canonical `<pluginId>:<realId>`
|
|
431
|
+
* shape used everywhere outside the provider boundary. Mirrors
|
|
432
|
+
* {@link buildMailId} so the two surfaces share a wire-format pattern.
|
|
433
|
+
*
|
|
434
|
+
* No escaping is applied: pluginIds must not contain `:` (enforced
|
|
435
|
+
* here); realIds may contain `:` and the parser treats them opaquely.
|
|
436
|
+
*/
|
|
437
|
+
export declare function buildCalendarEventId(pluginId: string, realId: string): string;
|
|
438
|
+
/**
|
|
439
|
+
* Parse a calendar event id back into its two segments. Splits on the
|
|
440
|
+
* **first** `:` so a realId that happens to contain colons (rare but
|
|
441
|
+
* Graph does occasionally mint them) round-trips unchanged.
|
|
442
|
+
*
|
|
443
|
+
* @throws If the id has no `:` separator or either segment is empty.
|
|
444
|
+
* The error names the offending input so a bad id surfaced from
|
|
445
|
+
* agent args produces an actionable tool failure.
|
|
446
|
+
*/
|
|
447
|
+
export declare function parseCalendarEventId(id: string): {
|
|
448
|
+
readonly pluginId: string;
|
|
449
|
+
readonly realId: string;
|
|
450
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Construct a calendar event id in the canonical `<pluginId>:<realId>`
|
|
3
|
+
* shape used everywhere outside the provider boundary. Mirrors
|
|
4
|
+
* {@link buildMailId} so the two surfaces share a wire-format pattern.
|
|
5
|
+
*
|
|
6
|
+
* No escaping is applied: pluginIds must not contain `:` (enforced
|
|
7
|
+
* here); realIds may contain `:` and the parser treats them opaquely.
|
|
8
|
+
*/
|
|
9
|
+
export function buildCalendarEventId(pluginId, realId) {
|
|
10
|
+
if (pluginId.length === 0 || pluginId.includes(":")) {
|
|
11
|
+
throw new Error(`pluginId must be non-empty and ":"-free: "${pluginId}"`);
|
|
12
|
+
}
|
|
13
|
+
if (realId.length === 0) {
|
|
14
|
+
throw new Error("realId must be non-empty");
|
|
15
|
+
}
|
|
16
|
+
return `${pluginId}:${realId}`;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Parse a calendar event id back into its two segments. Splits on the
|
|
20
|
+
* **first** `:` so a realId that happens to contain colons (rare but
|
|
21
|
+
* Graph does occasionally mint them) round-trips unchanged.
|
|
22
|
+
*
|
|
23
|
+
* @throws If the id has no `:` separator or either segment is empty.
|
|
24
|
+
* The error names the offending input so a bad id surfaced from
|
|
25
|
+
* agent args produces an actionable tool failure.
|
|
26
|
+
*/
|
|
27
|
+
export function parseCalendarEventId(id) {
|
|
28
|
+
const first = id.indexOf(":");
|
|
29
|
+
if (first <= 0) {
|
|
30
|
+
throw new Error(`calendar event id "${id}" is malformed: expected "<pluginId>:<realId>"`);
|
|
31
|
+
}
|
|
32
|
+
const realId = id.slice(first + 1);
|
|
33
|
+
if (realId.length === 0) {
|
|
34
|
+
throw new Error(`calendar event id "${id}" is malformed: empty real-id segment`);
|
|
35
|
+
}
|
|
36
|
+
return { pluginId: id.slice(0, first), realId };
|
|
37
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent chat history. Each row in the `chatmessages` table is one
|
|
3
|
+
* turn in a conversation between the user and the assistant on some
|
|
4
|
+
* channel (cli, telegram, …).
|
|
5
|
+
*
|
|
6
|
+
* The table intentionally has no `channel_id` column — channel is
|
|
7
|
+
* reachable by JOINing to `events.preferred_chat_channel_id`. This
|
|
8
|
+
* keeps the container side channel-blind: the agent's `send_chat` tool
|
|
9
|
+
* only needs to know the originating event id, never the channel.
|
|
10
|
+
*/
|
|
11
|
+
/** Sender role for a chat message. */
|
|
12
|
+
export type ChatRole = "user" | "assistant";
|
|
13
|
+
/**
|
|
14
|
+
* Shape of a row in the `chatmessages` table after JSON-decoding from
|
|
15
|
+
* postgres. `id` and `eventId` are kept as strings because postgres
|
|
16
|
+
* `bigint` exceeds JavaScript's safe integer range.
|
|
17
|
+
*/
|
|
18
|
+
export interface ChatMessage {
|
|
19
|
+
/** Database primary key. */
|
|
20
|
+
readonly id: string;
|
|
21
|
+
/** FK to {@link import("./Event").EventRow.id} — the event whose lifecycle this message belongs to. */
|
|
22
|
+
readonly eventId: string;
|
|
23
|
+
/** `'user'` or `'assistant'`. */
|
|
24
|
+
readonly role: ChatRole;
|
|
25
|
+
/** The message text. */
|
|
26
|
+
readonly textContent: string;
|
|
27
|
+
/** Insert timestamp. */
|
|
28
|
+
readonly createdAt: Date;
|
|
29
|
+
/**
|
|
30
|
+
* When ANY listener returned `true` from its async handler. `null`
|
|
31
|
+
* while the message has not yet been delivered to any subscriber.
|
|
32
|
+
* New subscribers replay all undelivered matching rows on
|
|
33
|
+
* registration so a message produced when nobody was listening is
|
|
34
|
+
* still delivered later.
|
|
35
|
+
*/
|
|
36
|
+
readonly deliveredAt: Date | null;
|
|
37
|
+
}
|
|
38
|
+
/** Input shape for {@link import("./ChatMessageBus").ChatMessageBus.insertAssistantMessage}. */
|
|
39
|
+
export interface NewChatMessage {
|
|
40
|
+
readonly eventId: string;
|
|
41
|
+
readonly role: ChatRole;
|
|
42
|
+
readonly textContent: string;
|
|
43
|
+
}
|
|
44
|
+
/** Filter for {@link import("./ChatMessageBus").ChatMessageBus.subscribe}. */
|
|
45
|
+
export interface ChatFilter {
|
|
46
|
+
/**
|
|
47
|
+
* Channel id (matched against the parent event's
|
|
48
|
+
* `preferred_chat_channel_id`). Omit to match any channel.
|
|
49
|
+
*/
|
|
50
|
+
readonly channelId?: string;
|
|
51
|
+
/** Only deliver messages of this role. Omit to match any role. */
|
|
52
|
+
readonly role?: ChatRole;
|
|
53
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent chat history. Each row in the `chatmessages` table is one
|
|
3
|
+
* turn in a conversation between the user and the assistant on some
|
|
4
|
+
* channel (cli, telegram, …).
|
|
5
|
+
*
|
|
6
|
+
* The table intentionally has no `channel_id` column — channel is
|
|
7
|
+
* reachable by JOINing to `events.preferred_chat_channel_id`. This
|
|
8
|
+
* keeps the container side channel-blind: the agent's `send_chat` tool
|
|
9
|
+
* only needs to know the originating event id, never the channel.
|
|
10
|
+
*/
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { ChatFilter, ChatMessage, NewChatMessage } from "./ChatMessage.js";
|
|
2
|
+
import type { Logger } from "./logging/Logger.js";
|
|
3
|
+
import type { PostgresConnection } from "./PostgresConnection.js";
|
|
4
|
+
/**
|
|
5
|
+
* Disposer returned by {@link ChatMessageBus.subscribe}. Calling it
|
|
6
|
+
* detaches the underlying NOTIFY handler. Idempotent: calling more than
|
|
7
|
+
* once is safe.
|
|
8
|
+
*/
|
|
9
|
+
export type ChatUnsubscribe = () => Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* Async handler invoked for every chat message matching a subscription's
|
|
12
|
+
* filter. Returns `true` once the message is considered delivered to the
|
|
13
|
+
* end user; the bus then sets `delivered_at`. Returning `false` (or
|
|
14
|
+
* throwing) leaves the row undelivered so a later subscriber can pick
|
|
15
|
+
* it up on its replay.
|
|
16
|
+
*/
|
|
17
|
+
export type ChatHandler = (message: ChatMessage) => Promise<boolean>;
|
|
18
|
+
/**
|
|
19
|
+
* Domain client for the `chatmessages` table.
|
|
20
|
+
*
|
|
21
|
+
* Three responsibilities:
|
|
22
|
+
*
|
|
23
|
+
* 1. Persistence of assistant replies (`insertAssistantMessage`). User
|
|
24
|
+
* messages are persisted by `EventBus.add` directly when the event
|
|
25
|
+
* has `isChat=true`, so they don't have a public method here.
|
|
26
|
+
* 2. History retrieval for the agent (`fetchHistoryForEvent`). Returns
|
|
27
|
+
* every message on the same channel as the given event, oldest
|
|
28
|
+
* first. The container's `ChatManager` wraps this for the LLM.
|
|
29
|
+
* 3. At-least-once channel delivery (`subscribe`). New subscribers
|
|
30
|
+
* replay all undelivered matching rows on registration; live rows
|
|
31
|
+
* arrive via the `chatmessages_new` NOTIFY channel. A handler
|
|
32
|
+
* returning `true` marks the row delivered; multiple matching
|
|
33
|
+
* listeners is fine — the UPDATE is idempotent.
|
|
34
|
+
*
|
|
35
|
+
* The bus has no notion of channel-binding: callers pass `eventId` or
|
|
36
|
+
* the full filter on each call. Channel routing happens via JOIN to
|
|
37
|
+
* `events.preferred_chat_channel_id`, NOT a denormalized column on
|
|
38
|
+
* `chatmessages`.
|
|
39
|
+
*/
|
|
40
|
+
export declare class ChatMessageBus {
|
|
41
|
+
private readonly connection;
|
|
42
|
+
private readonly log;
|
|
43
|
+
constructor(connection: PostgresConnection, log?: Logger);
|
|
44
|
+
/**
|
|
45
|
+
* Insert one assistant reply. Returns the persisted row. Used by
|
|
46
|
+
* the container's `send_chat` tool via `ChatManager`.
|
|
47
|
+
*/
|
|
48
|
+
insertAssistantMessage(eventId: string, textContent: string): Promise<ChatMessage>;
|
|
49
|
+
/**
|
|
50
|
+
* Insert a row of any role. The atomic-with-event INSERT path used
|
|
51
|
+
* by `EventBus.add` for user messages bypasses this method (it
|
|
52
|
+
* issues the INSERT inside its own transaction); this is provided
|
|
53
|
+
* for tests and for callers that already hold their own
|
|
54
|
+
* transaction.
|
|
55
|
+
*/
|
|
56
|
+
insert(input: NewChatMessage): Promise<ChatMessage>;
|
|
57
|
+
/** Fetch one row by id. Returns `undefined` if not found. */
|
|
58
|
+
getById(id: string): Promise<ChatMessage | undefined>;
|
|
59
|
+
/**
|
|
60
|
+
* Return every chat message on the same channel as the given event,
|
|
61
|
+
* oldest first. The channel is resolved via the join
|
|
62
|
+
* `chatmessages.event_id → events.preferred_chat_channel_id`,
|
|
63
|
+
* which lets the container request "the conversation this agentrun
|
|
64
|
+
* is part of" without ever naming a channel itself.
|
|
65
|
+
*
|
|
66
|
+
* If the parent event has no `preferred_chat_channel_id`, only
|
|
67
|
+
* messages tied to that exact event are returned (no cross-event
|
|
68
|
+
* thread to attach to).
|
|
69
|
+
*/
|
|
70
|
+
fetchHistoryForEvent(eventId: string): Promise<ChatMessage[]>;
|
|
71
|
+
/**
|
|
72
|
+
* Subscribe to chat messages matching `filter`. Returns an
|
|
73
|
+
* unsubscribe function.
|
|
74
|
+
*
|
|
75
|
+
* Delivery semantics:
|
|
76
|
+
*
|
|
77
|
+
* - LISTEN is attached BEFORE the replay SELECT, so any row inserted
|
|
78
|
+
* between SELECT and attach won't be missed. NOTIFY-driven
|
|
79
|
+
* delivery is still gated by `delivered_at IS NULL`, so the same
|
|
80
|
+
* row is never delivered twice via this subscription.
|
|
81
|
+
* - The replay SELECT returns every undelivered matching row in
|
|
82
|
+
* `created_at` order; each is awaited through the handler.
|
|
83
|
+
* - When the handler returns `true`, the bus runs
|
|
84
|
+
* `UPDATE chatmessages SET delivered_at = now() WHERE id = $1
|
|
85
|
+
* AND delivered_at IS NULL` — a no-op if a concurrent subscriber
|
|
86
|
+
* has already marked it.
|
|
87
|
+
* - Handler errors are caught and logged so one bad subscriber
|
|
88
|
+
* doesn't break others.
|
|
89
|
+
*
|
|
90
|
+
* Multiple matching subscribers will each see the row; the first
|
|
91
|
+
* to return `true` "wins" the delivery write. This is idempotent
|
|
92
|
+
* and matches the at-least-once contract: handlers must tolerate
|
|
93
|
+
* being called for an already-displayed message.
|
|
94
|
+
*/
|
|
95
|
+
subscribe(filter: ChatFilter, handler: ChatHandler): Promise<ChatUnsubscribe>;
|
|
96
|
+
/**
|
|
97
|
+
* Parse a NOTIFY payload, prefilter on the prefix, fetch the row,
|
|
98
|
+
* and hand it to the dispatcher. The `seenLive` set deduplicates
|
|
99
|
+
* against the replay SELECT so a row picked up by both paths is
|
|
100
|
+
* delivered exactly once per subscription.
|
|
101
|
+
*/
|
|
102
|
+
private dispatchNotification;
|
|
103
|
+
/**
|
|
104
|
+
* SELECT all undelivered rows that match `filter`, oldest first.
|
|
105
|
+
* Channel filter joins to `events.preferred_chat_channel_id`.
|
|
106
|
+
*/
|
|
107
|
+
private fetchUndelivered;
|
|
108
|
+
/** Conditional UPDATE to set delivered_at; no-op if already set. */
|
|
109
|
+
private markDelivered;
|
|
110
|
+
}
|