@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.
Files changed (83) hide show
  1. package/build/AgentRun.d.ts +187 -0
  2. package/build/AgentRun.js +1 -0
  3. package/build/AgentRunBus.d.ts +147 -0
  4. package/build/AgentRunBus.js +362 -0
  5. package/build/Calendar.d.ts +450 -0
  6. package/build/Calendar.js +37 -0
  7. package/build/ChatMessage.d.ts +53 -0
  8. package/build/ChatMessage.js +11 -0
  9. package/build/ChatMessageBus.d.ts +110 -0
  10. package/build/ChatMessageBus.js +257 -0
  11. package/build/Config.d.ts +83 -0
  12. package/build/Config.js +1 -0
  13. package/build/ContainerTool.d.ts +34 -0
  14. package/build/ContainerTool.js +1 -0
  15. package/build/CronExpression.d.ts +53 -0
  16. package/build/CronExpression.js +77 -0
  17. package/build/Event.d.ts +280 -0
  18. package/build/Event.js +14 -0
  19. package/build/EventBus.d.ts +198 -0
  20. package/build/EventBus.js +482 -0
  21. package/build/HandlerCatalog.d.ts +75 -0
  22. package/build/HandlerCatalog.js +165 -0
  23. package/build/InferenceEvent.d.ts +30 -0
  24. package/build/InferenceEvent.js +1 -0
  25. package/build/InferenceEventBus.d.ts +39 -0
  26. package/build/InferenceEventBus.js +80 -0
  27. package/build/Mail.d.ts +250 -0
  28. package/build/Mail.js +73 -0
  29. package/build/MailStyleTemplate.d.ts +71 -0
  30. package/build/MailStyleTemplate.js +47 -0
  31. package/build/ModelMetaData.d.ts +63 -0
  32. package/build/ModelMetaData.js +12 -0
  33. package/build/PathGlob.d.ts +20 -0
  34. package/build/PathGlob.js +33 -0
  35. package/build/Plugin.d.ts +709 -0
  36. package/build/Plugin.js +29 -0
  37. package/build/PostgresConnection.d.ts +75 -0
  38. package/build/PostgresConnection.js +162 -0
  39. package/build/ScheduledSubagent.d.ts +49 -0
  40. package/build/ScheduledSubagent.js +1 -0
  41. package/build/ScheduledSubagentBus.d.ts +94 -0
  42. package/build/ScheduledSubagentBus.js +187 -0
  43. package/build/Schema.d.ts +84 -0
  44. package/build/Schema.js +570 -0
  45. package/build/StepResult.d.ts +103 -0
  46. package/build/StepResult.js +1 -0
  47. package/build/StepResultBus.d.ts +64 -0
  48. package/build/StepResultBus.js +169 -0
  49. package/build/Timezone.d.ts +58 -0
  50. package/build/Timezone.js +84 -0
  51. package/build/ToolCall.d.ts +25 -0
  52. package/build/ToolCall.js +1 -0
  53. package/build/ToolCallBus.d.ts +65 -0
  54. package/build/ToolCallBus.js +110 -0
  55. package/build/ToolLevel.d.ts +20 -0
  56. package/build/ToolLevel.js +2 -0
  57. package/build/ToolRunner.d.ts +118 -0
  58. package/build/ToolRunner.js +192 -0
  59. package/build/ToolsExpressionUtils.d.ts +80 -0
  60. package/build/ToolsExpressionUtils.js +102 -0
  61. package/build/WorkspaceFile.d.ts +104 -0
  62. package/build/WorkspaceFile.js +7 -0
  63. package/build/estimateTokens.d.ts +16 -0
  64. package/build/estimateTokens.js +18 -0
  65. package/build/index.d.ts +37 -0
  66. package/build/index.js +26 -0
  67. package/build/logging/Logger.d.ts +66 -0
  68. package/build/logging/Logger.js +90 -0
  69. package/build/markdownHtml.d.ts +14 -0
  70. package/build/markdownHtml.js +21 -0
  71. package/build/markdownTerminal.d.ts +14 -0
  72. package/build/markdownTerminal.js +376 -0
  73. package/build/testing/MockAgentRunBus.d.ts +52 -0
  74. package/build/testing/MockAgentRunBus.js +175 -0
  75. package/build/testing/MockBusStore.d.ts +22 -0
  76. package/build/testing/MockBusStore.js +22 -0
  77. package/build/testing/MockEventBus.d.ts +20 -0
  78. package/build/testing/MockEventBus.js +57 -0
  79. package/build/testing/MockLogger.d.ts +52 -0
  80. package/build/testing/MockLogger.js +65 -0
  81. package/build/testing/index.d.ts +4 -0
  82. package/build/testing/index.js +4 -0
  83. 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
+ }