@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
package/build/Event.d.ts
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lifecycle states for an `events` row.
|
|
3
|
+
*
|
|
4
|
+
* Events are the immutable record of "the world said this happened".
|
|
5
|
+
* They no longer participate in any worker state machine themselves —
|
|
6
|
+
* the actual work runs in `agentruns` rows that reference the event.
|
|
7
|
+
*
|
|
8
|
+
* Transitions:
|
|
9
|
+
*
|
|
10
|
+
* - `pending`: row inserted by a host plugin via {@link EventBus.add}.
|
|
11
|
+
* - `running`: the container's input-event watcher has spawned the root
|
|
12
|
+
* agentrun for this event.
|
|
13
|
+
* - `done` / `failed`: set reactively when the last `pending` /
|
|
14
|
+
* `running` agentrun for this event settles. `failed` wins if any
|
|
15
|
+
* agentrun for the event has failed.
|
|
16
|
+
*/
|
|
17
|
+
export type EventState = "pending" | "running" | "done" | "failed";
|
|
18
|
+
/**
|
|
19
|
+
* Standard priority tiers for events. Higher = processed first.
|
|
20
|
+
* Producers should pick one of these rather than passing a bare number,
|
|
21
|
+
* unless they have a specific reason. The SQL default for `events.priority`
|
|
22
|
+
* is 50, matching `EVENT_PRIORITY.ASYNC`.
|
|
23
|
+
*/
|
|
24
|
+
export declare const EVENT_PRIORITY: {
|
|
25
|
+
/** Direct user chat (telegram, cli-chat). Preempts async work. */
|
|
26
|
+
readonly CHAT: 100;
|
|
27
|
+
/** Asynchronous external input — mail, group chat. Matches the SQL default. */
|
|
28
|
+
readonly ASYNC: 50;
|
|
29
|
+
/** Cron-driven background work. Yields to chat and async. */
|
|
30
|
+
readonly BACKGROUND: 10;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Shape of a row in the `events` table after JSON-decoding from postgres.
|
|
34
|
+
* The numeric `id` is kept as a string because postgres `bigint` exceeds
|
|
35
|
+
* JavaScript's safe integer range.
|
|
36
|
+
*/
|
|
37
|
+
export interface EventRow {
|
|
38
|
+
/**
|
|
39
|
+
* Database primary key. `bigserial` is returned as a string because
|
|
40
|
+
* postgres `bigint` exceeds JavaScript's safe integer range.
|
|
41
|
+
*/
|
|
42
|
+
readonly id: string;
|
|
43
|
+
/**
|
|
44
|
+
* Event topic, matching `\w+(:\w+)*` (e.g. `mail`, `chat:whatsapp`,
|
|
45
|
+
* `chat:telegram:group:reaction`). Each `:`-separated segment maps
|
|
46
|
+
* to a folder when the container resolves the handler markdown.
|
|
47
|
+
*/
|
|
48
|
+
readonly topic: string;
|
|
49
|
+
/**
|
|
50
|
+
* Higher = processed first within a queue. FIFO within priority.
|
|
51
|
+
* Default 50. Inherited by all agentruns spawned for this event.
|
|
52
|
+
*/
|
|
53
|
+
readonly priority: number;
|
|
54
|
+
/** Lifecycle state — see {@link EventState}. */
|
|
55
|
+
readonly state: EventState;
|
|
56
|
+
/**
|
|
57
|
+
* Event-specific data; arbitrary JSON. Schema is owned by the
|
|
58
|
+
* producing plugin / topic, not by the bus.
|
|
59
|
+
*/
|
|
60
|
+
readonly payload: unknown;
|
|
61
|
+
/**
|
|
62
|
+
* Globally unique key for dedup. `null` when the producer didn't
|
|
63
|
+
* supply one (then no dedup happens).
|
|
64
|
+
*/
|
|
65
|
+
readonly idempotencyKey: string | null;
|
|
66
|
+
/**
|
|
67
|
+
* Whether this event is a chat message from the user. When `true`,
|
|
68
|
+
* `EventBus.add` mirrors `prompt` into `chatmessages` (role
|
|
69
|
+
* `'user'`) in the same transaction as the event INSERT, so the
|
|
70
|
+
* agent picks the message up via chat history rather than via the
|
|
71
|
+
* agentrun's `prompt` field.
|
|
72
|
+
*/
|
|
73
|
+
readonly isChat: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Channel the assistant should reply on (e.g. `"cli"`, `"telegram"`).
|
|
76
|
+
* Stamped host-side at emit time, falling back to
|
|
77
|
+
* `DEFAULT_CHAT_CHANNEL_ID`. The container never reads this field;
|
|
78
|
+
* routing happens via JOIN in the chatmessages trigger and in
|
|
79
|
+
* `ChatMessageBus`. May be `null` for non-chat events whose source
|
|
80
|
+
* plugin didn't set a default routing target.
|
|
81
|
+
*/
|
|
82
|
+
readonly preferredChatChannelId: string | null;
|
|
83
|
+
/**
|
|
84
|
+
* The event's primary user-visible text, written by the emitter.
|
|
85
|
+
* Always populated:
|
|
86
|
+
*
|
|
87
|
+
* - For chat events (`isChat=true`): the user's chat message. Also
|
|
88
|
+
* mirrored into `chatmessages.text_content` (role `'user'`) by
|
|
89
|
+
* `EventBus.add`, which is the channel the AgentRunner consumes.
|
|
90
|
+
* - For non-chat events: a first-person framing of what happened
|
|
91
|
+
* ("A new mail from Anna arrived: …"). The EventWatcher copies
|
|
92
|
+
* it into the root agentrun's `prompt` so the AgentRunner can
|
|
93
|
+
* append it as the trailing user message.
|
|
94
|
+
*/
|
|
95
|
+
readonly prompt: string;
|
|
96
|
+
/**
|
|
97
|
+
* Override for the root agentrun's handler. `null` means the
|
|
98
|
+
* input-event watcher uses `'index'` (i.e. resolves
|
|
99
|
+
* `<topic>/index.md`). Set by the emitter via
|
|
100
|
+
* {@link NewEvent.startHandler}; never mutated after insert.
|
|
101
|
+
*/
|
|
102
|
+
readonly startHandler: string | null;
|
|
103
|
+
/**
|
|
104
|
+
* `true` when this event was emitted by a trusted user-input source
|
|
105
|
+
* (the operator at the local terminal via cli-chat, the operator on
|
|
106
|
+
* Telegram). Stamped at emit time, never mutated. Propagated verbatim
|
|
107
|
+
* to the root agentrun and to every child agentrun spawned via
|
|
108
|
+
* `schedule_subagent` / `start_subagent`, so future system tools can gate
|
|
109
|
+
* risky reads / writes
|
|
110
|
+
* (editing SOUL.md, etc.) on whether the run descends from a trusted
|
|
111
|
+
* input. `false` for everything else (mail, webhooks, cron-driven
|
|
112
|
+
* workflows).
|
|
113
|
+
*/
|
|
114
|
+
readonly privileged: boolean;
|
|
115
|
+
/**
|
|
116
|
+
* When `true` and the event terminates in `failed`, the host writes
|
|
117
|
+
* a `role='assistant'` chatmessage `"Something went wrong processing
|
|
118
|
+
* the chat message: <error>"` to this event before signalling settle.
|
|
119
|
+
* Lets chat plugins surface failures through the ordinary chat
|
|
120
|
+
* subscription path without each needing its own catch-block
|
|
121
|
+
* rendering. No-op on `done`.
|
|
122
|
+
*/
|
|
123
|
+
readonly outputChatOnFailure: boolean;
|
|
124
|
+
/** Insert timestamp — postgres `now()` at INSERT. */
|
|
125
|
+
readonly createdAt: Date;
|
|
126
|
+
/** Last `update()` timestamp — bumped to `now()` on every update. */
|
|
127
|
+
readonly updatedAt: Date;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* One auxiliary file to attach to an event at emit time. The host writes
|
|
131
|
+
* each file under `<scratchDir>/<eventId>/<name>` inside the same
|
|
132
|
+
* transaction that inserts the event row, so the container watcher only
|
|
133
|
+
* ever observes events whose files are already on disk at
|
|
134
|
+
* `/scratch/<event-id>/<name>` inside both the agent and every MCP
|
|
135
|
+
* container.
|
|
136
|
+
*
|
|
137
|
+
* Two arms:
|
|
138
|
+
* - `contents` — Buffer the host writes directly. Use when bytes are
|
|
139
|
+
* already in memory (e.g. just decoded from a base64 MCP response).
|
|
140
|
+
* - `sourcePath` — absolute host path to a file the plugin has already
|
|
141
|
+
* written to disk. The host **moves** the file into the scratch dir
|
|
142
|
+
* (`fs.rename`, falling back to copy+unlink across filesystems); the
|
|
143
|
+
* plugin gives up ownership. Use for large files the plugin doesn't
|
|
144
|
+
* want to re-buffer in memory.
|
|
145
|
+
*
|
|
146
|
+
* `name` is a basename. Path separators and `..` segments are rejected
|
|
147
|
+
* at emit; on collision within the same emit, the second file overwrites
|
|
148
|
+
* the first (caller's responsibility to keep names unique).
|
|
149
|
+
*/
|
|
150
|
+
export type EventFile = {
|
|
151
|
+
readonly name: string;
|
|
152
|
+
readonly contents: Buffer;
|
|
153
|
+
} | {
|
|
154
|
+
readonly name: string;
|
|
155
|
+
readonly sourcePath: string;
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* Input shape for {@link EventBus.add}.
|
|
159
|
+
*
|
|
160
|
+
* `prompt` carries the event's primary user-visible text — for chat
|
|
161
|
+
* events it's the user's message; for everything else it's a
|
|
162
|
+
* first-person framing of what happened ("A new mail from Anna
|
|
163
|
+
* arrived: …"). `EventBus.add` validates that the value is non-empty
|
|
164
|
+
* after trimming, so plugins that bypass the type system still surface
|
|
165
|
+
* the bug at emit time instead of leaving the AgentRunner with an
|
|
166
|
+
* empty messages array.
|
|
167
|
+
*
|
|
168
|
+
* `payload` is optional structured supplementary data (Telegram
|
|
169
|
+
* `update_id`, WhatsApp `group_jid`, etc.) — it never carries the
|
|
170
|
+
* agent-visible text directly any more.
|
|
171
|
+
*
|
|
172
|
+
* `isChat` is just a flag. When `true`, `EventBus.add` mirrors
|
|
173
|
+
* `prompt` into the `chatmessages` table so the AgentRunner picks the
|
|
174
|
+
* message up via chat history; the agentrun's own `prompt` field
|
|
175
|
+
* stays null for chat events to avoid double-counting the trailing
|
|
176
|
+
* turn. The flag does not change the input shape — both arms have
|
|
177
|
+
* identical fields — so this stays a single interface rather than a
|
|
178
|
+
* discriminated union.
|
|
179
|
+
*/
|
|
180
|
+
export interface NewEvent {
|
|
181
|
+
readonly topic: string;
|
|
182
|
+
/**
|
|
183
|
+
* Primary user-visible text. Required, non-empty after trimming.
|
|
184
|
+
* See {@link NewEvent} for how it's persisted on each arm.
|
|
185
|
+
*/
|
|
186
|
+
readonly prompt: string;
|
|
187
|
+
/** Optional structured supplementary data; arbitrary JSON. */
|
|
188
|
+
readonly payload?: unknown;
|
|
189
|
+
/** Higher = processed first; default 50. */
|
|
190
|
+
readonly priority?: number;
|
|
191
|
+
/** Initial state; default `"pending"`. */
|
|
192
|
+
readonly state?: EventState;
|
|
193
|
+
/** Globally unique key for dedup; null = no dedup. */
|
|
194
|
+
readonly idempotencyKey?: string;
|
|
195
|
+
/**
|
|
196
|
+
* Whether this event is a chat message from the user. Default
|
|
197
|
+
* `false`. See {@link NewEvent} for the persistence consequences.
|
|
198
|
+
*/
|
|
199
|
+
readonly isChat?: boolean;
|
|
200
|
+
/**
|
|
201
|
+
* Channel id the assistant should reply on. Plugins typically set
|
|
202
|
+
* this to their own channel name (e.g. `"cli"`). When omitted, the
|
|
203
|
+
* host's `HostContextImpl.emit` stamps `DEFAULT_CHAT_CHANNEL_ID`.
|
|
204
|
+
*
|
|
205
|
+
* If the producer is not a chat plugin, this should be `null` or omitted.
|
|
206
|
+
*/
|
|
207
|
+
readonly preferredChatChannelId?: string | null;
|
|
208
|
+
/**
|
|
209
|
+
* Whether this event originates from a trusted user-input source.
|
|
210
|
+
* Default `false` — the SQL column default covers omission. See
|
|
211
|
+
* {@link EventRow.privileged} for the trust-model rationale.
|
|
212
|
+
*/
|
|
213
|
+
readonly privileged?: boolean;
|
|
214
|
+
/**
|
|
215
|
+
* When `true` and the event terminates in `failed`, the host writes
|
|
216
|
+
* a `role='assistant'` chatmessage carrying the failure text to this
|
|
217
|
+
* event before `handle.settled` rejects. Default `false` — the SQL
|
|
218
|
+
* column default covers omission. Chat plugins (cli-chat, telegram)
|
|
219
|
+
* set this to `true` so users see failures through the same chat
|
|
220
|
+
* subscription path that delivers normal replies. See
|
|
221
|
+
* {@link EventRow.outputChatOnFailure}.
|
|
222
|
+
*/
|
|
223
|
+
readonly outputChatOnFailure?: boolean;
|
|
224
|
+
/**
|
|
225
|
+
* When set, EventBus.add inserts a `role='user'` chatmessage with
|
|
226
|
+
* this text in the same transaction as the event INSERT.
|
|
227
|
+
*
|
|
228
|
+
* Atomicity matters: a post-emit chatmessages INSERT acquires an
|
|
229
|
+
* FK row-lock on the events row that conflicts with the input-event
|
|
230
|
+
* watcher's `FOR UPDATE SKIP LOCKED` claim. If the watcher races
|
|
231
|
+
* the chatmessage write, `SKIP LOCKED` skips the row and — because
|
|
232
|
+
* the events_new NOTIFY has already been consumed — the event sits
|
|
233
|
+
* pending forever. Doing the chatmessage INSERT inside the same
|
|
234
|
+
* transaction closes the window: the FK lock is released before
|
|
235
|
+
* NOTIFY events_new fires post-commit.
|
|
236
|
+
*
|
|
237
|
+
* Use this for plugin emitters that want a chatmessage text
|
|
238
|
+
* different from `prompt` (e.g. cli-chat persists the slash-
|
|
239
|
+
* prefixed `/topic/handler …` line while the handler sees only
|
|
240
|
+
* the trailing prompt text). When unset and `isChat: true`, the
|
|
241
|
+
* existing path inserts `prompt` itself.
|
|
242
|
+
*/
|
|
243
|
+
readonly userChatMessage?: string;
|
|
244
|
+
/**
|
|
245
|
+
* Override which handler markdown the root agentrun runs. When
|
|
246
|
+
* omitted, the input-event watcher uses `'index'` (i.e. resolves
|
|
247
|
+
* `<topic>/index.md`). Useful when an emitter wants to skip the
|
|
248
|
+
* triage `index.md` and invoke a specific handler directly.
|
|
249
|
+
*
|
|
250
|
+
* Pass the basename without `.md` (e.g. `'analyze'` resolves to
|
|
251
|
+
* `<topic>/analyze.md`). The agentrun fails loud at handler-load
|
|
252
|
+
* time if no such file exists — there is no shape validation
|
|
253
|
+
* here, mirroring how `topic` is only regex-checked, not
|
|
254
|
+
* existence-checked, at emit time.
|
|
255
|
+
*/
|
|
256
|
+
readonly startHandler?: string;
|
|
257
|
+
/**
|
|
258
|
+
* Auxiliary files to stage at `/scratch/<event-id>/<name>` for this
|
|
259
|
+
* event. Visible to the agent and to every MCP container under the
|
|
260
|
+
* same absolute path. Staging happens atomically with the event
|
|
261
|
+
* INSERT (inside the same transaction), so the container watcher
|
|
262
|
+
* never sees an event whose files aren't yet on disk. See {@link
|
|
263
|
+
* EventFile} for the two payload shapes.
|
|
264
|
+
*/
|
|
265
|
+
readonly files?: readonly EventFile[];
|
|
266
|
+
}
|
|
267
|
+
/** Patch shape for {@link EventBus.update}. */
|
|
268
|
+
export interface EventPatch {
|
|
269
|
+
readonly state?: EventState;
|
|
270
|
+
readonly payload?: unknown;
|
|
271
|
+
readonly priority?: number;
|
|
272
|
+
readonly preferredChatChannelId?: string | null;
|
|
273
|
+
}
|
|
274
|
+
/** Filter for {@link EventBus.waitForNext}. */
|
|
275
|
+
export interface EventFilter {
|
|
276
|
+
/** If set, only events whose topic is in this list will match. */
|
|
277
|
+
readonly topics?: readonly string[];
|
|
278
|
+
/** If set, only events in one of these states match (default `["pending"]`). */
|
|
279
|
+
readonly states?: readonly EventState[];
|
|
280
|
+
}
|
package/build/Event.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standard priority tiers for events. Higher = processed first.
|
|
3
|
+
* Producers should pick one of these rather than passing a bare number,
|
|
4
|
+
* unless they have a specific reason. The SQL default for `events.priority`
|
|
5
|
+
* is 50, matching `EVENT_PRIORITY.ASYNC`.
|
|
6
|
+
*/
|
|
7
|
+
export const EVENT_PRIORITY = {
|
|
8
|
+
/** Direct user chat (telegram, cli-chat). Preempts async work. */
|
|
9
|
+
CHAT: 100,
|
|
10
|
+
/** Asynchronous external input — mail, group chat. Matches the SQL default. */
|
|
11
|
+
ASYNC: 50,
|
|
12
|
+
/** Cron-driven background work. Yields to chat and async. */
|
|
13
|
+
BACKGROUND: 10,
|
|
14
|
+
};
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import type { EventFilter, EventPatch, EventRow, EventState, NewEvent } from "./Event.js";
|
|
2
|
+
import type { Logger } from "./logging/Logger.js";
|
|
3
|
+
import type { PostgresConnection } from "./PostgresConnection.js";
|
|
4
|
+
/**
|
|
5
|
+
* Thrown by {@link EventBus.add} when an event's `idempotencyKey`
|
|
6
|
+
* collides with one already in the `events` table (Postgres unique
|
|
7
|
+
* violation `23505` on `events_idempotency_key_key`).
|
|
8
|
+
*
|
|
9
|
+
* The error is deliberately typed rather than swallowed: an emitter
|
|
10
|
+
* that re-derives the same key on a re-walk (the ms365 mail delta poll,
|
|
11
|
+
* the calendar poll, chat compaction, …) is expected to `catch` this
|
|
12
|
+
* and treat the collision as a no-op — the event already exists. Other
|
|
13
|
+
* emitters that should never collide let it propagate as a real fault.
|
|
14
|
+
*/
|
|
15
|
+
export declare class DuplicateIdempotencyKeyError extends Error {
|
|
16
|
+
/** The colliding idempotency key, for logging by the catching emitter. */
|
|
17
|
+
readonly idempotencyKey: string;
|
|
18
|
+
/**
|
|
19
|
+
* @param idempotencyKey - The key that already existed in `events`.
|
|
20
|
+
*/
|
|
21
|
+
constructor(idempotencyKey: string);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Domain client for the `events` table that backs the host ↔ container bus.
|
|
25
|
+
*
|
|
26
|
+
* Uses an injected {@link PostgresConnection} for pool + LISTEN access;
|
|
27
|
+
* does not own connection lifecycle. Several `EventBus` instances can
|
|
28
|
+
* share a connection, and the connection can be reused by other domain
|
|
29
|
+
* clients (e.g. {@link AgentRunBus}) without opening more sockets.
|
|
30
|
+
*
|
|
31
|
+
* Listens on `events_new` for wake-ups — the channel that fires on
|
|
32
|
+
* INSERT into `events`. State updates (which fire on `events_state`)
|
|
33
|
+
* are not consumed here; host plugins that need to wait for an event to
|
|
34
|
+
* settle should subscribe to that channel directly via
|
|
35
|
+
* {@link PostgresConnection.listen}.
|
|
36
|
+
*
|
|
37
|
+
* `waitForNext()` does an initial query before sleeping on the listen
|
|
38
|
+
* channel, so events that already existed when the call started are
|
|
39
|
+
* picked up immediately.
|
|
40
|
+
*
|
|
41
|
+
* The bus is *not* a multi-consumer queue: two waiters may observe the
|
|
42
|
+
* same event. Caller is responsible for transitioning state via
|
|
43
|
+
* {@link update} after handling.
|
|
44
|
+
*/
|
|
45
|
+
export declare class EventBus {
|
|
46
|
+
private readonly connection;
|
|
47
|
+
private readonly log;
|
|
48
|
+
private notifyWaiters;
|
|
49
|
+
private listenInstalled;
|
|
50
|
+
private readonly listenHandler;
|
|
51
|
+
/**
|
|
52
|
+
* @param connection - Shared pg pool / LISTEN handle.
|
|
53
|
+
* @param log - Optional logger; when set, NOTIFY arrivals are
|
|
54
|
+
* debug-logged. Omitted on the host-side bus that runs inside
|
|
55
|
+
* one-shot CLI commands so they stay quiet by default.
|
|
56
|
+
*/
|
|
57
|
+
constructor(connection: PostgresConnection, log?: Logger);
|
|
58
|
+
/**
|
|
59
|
+
* Apply the events + agentruns schema (idempotent). Safe to call on
|
|
60
|
+
* every daemon start. The same SQL bundle is consumed by
|
|
61
|
+
* {@link AgentRunBus.installSchema}, so calling either is sufficient.
|
|
62
|
+
*/
|
|
63
|
+
installSchema(): Promise<void>;
|
|
64
|
+
/**
|
|
65
|
+
* Insert a new event and return the persisted row.
|
|
66
|
+
*
|
|
67
|
+
* Transactional: when `event.isChat === true`, a `chatmessages`
|
|
68
|
+
* row (role `'user'`) carrying the same `prompt` text is inserted
|
|
69
|
+
* in the same transaction. Both NOTIFY triggers fire post-COMMIT,
|
|
70
|
+
* so listeners always observe consistent state.
|
|
71
|
+
*
|
|
72
|
+
* Validation: `prompt` must be a non-empty string after trimming —
|
|
73
|
+
* for any event, chat or not. The single check exists at this
|
|
74
|
+
* boundary so plugins that bypass the type system (untyped JS,
|
|
75
|
+
* unsafe casts) still surface the bug at emit time instead of
|
|
76
|
+
* leaving the AgentRunner with an empty messages array.
|
|
77
|
+
*
|
|
78
|
+
* `beforeCommit` runs **inside the transaction** after the event
|
|
79
|
+
* row (and any chat mirror) is inserted but before COMMIT. Use it
|
|
80
|
+
* for filesystem side-effects that must be atomic with the INSERT
|
|
81
|
+
* from the watcher's perspective — most importantly, staging
|
|
82
|
+
* `event.files` under `<scratchDir>/<row.id>/` so the container
|
|
83
|
+
* never observes an event without its scratch files in place.
|
|
84
|
+
* Throwing from `beforeCommit` rolls the INSERT back; cleaning up
|
|
85
|
+
* any partial filesystem state is the callback's responsibility.
|
|
86
|
+
*
|
|
87
|
+
* @throws {DuplicateIdempotencyKeyError} If `idempotencyKey` collides
|
|
88
|
+
* with an existing event. Emitters that re-derive stable keys on a
|
|
89
|
+
* re-walk are expected to catch this and treat it as a no-op.
|
|
90
|
+
* @throws If `topic` does not match `\w+(:\w+)*`, if `prompt` is
|
|
91
|
+
* missing or whitespace-only, or if `beforeCommit` throws.
|
|
92
|
+
*/
|
|
93
|
+
add(event: NewEvent, beforeCommit?: (row: EventRow) => Promise<void>): Promise<EventRow>;
|
|
94
|
+
/**
|
|
95
|
+
* Fetch one event by id. Returns `undefined` if not found. Mirrors
|
|
96
|
+
* {@link import("./StepResultBus").StepResultBus.getById} so
|
|
97
|
+
* notification consumers can promote a payload-only id into a full
|
|
98
|
+
* typed row without a hand-written query.
|
|
99
|
+
*/
|
|
100
|
+
getById(id: string): Promise<EventRow | undefined>;
|
|
101
|
+
/**
|
|
102
|
+
* Fetch all event rows whose `updated_at` is `>= since`, ordered by
|
|
103
|
+
* `(updated_at, id)`. Used by the polling-based report layer to
|
|
104
|
+
* pick up new inserts and state transitions without subscribing to
|
|
105
|
+
* NOTIFY. Returns an empty array when nothing changed since the
|
|
106
|
+
* anchor.
|
|
107
|
+
*/
|
|
108
|
+
listSince(since: Date): Promise<EventRow[]>;
|
|
109
|
+
/**
|
|
110
|
+
* Fetch the most recently created N event rows, newest first. Used by
|
|
111
|
+
* `./cli.sh events list` to surface recent bus activity for operators
|
|
112
|
+
* looking for an event id to inspect with `events report <id>`.
|
|
113
|
+
*
|
|
114
|
+
* @param limit Maximum number of rows to return. Must be a positive integer.
|
|
115
|
+
* @returns Up to `limit` event rows ordered by descending `(created_at, id)`.
|
|
116
|
+
*/
|
|
117
|
+
listLatest(limit: number): Promise<EventRow[]>;
|
|
118
|
+
/**
|
|
119
|
+
* Fetch the most recently created N event rows whose topic, start_handler,
|
|
120
|
+
* prompt, or payload (cast to text) case-insensitively contains `needle`.
|
|
121
|
+
* Used by `./cli.sh events list <search>` so the operator can grep the
|
|
122
|
+
* recent bus without piping through `grep`.
|
|
123
|
+
*
|
|
124
|
+
* Substring match via `ILIKE '%needle%'`; the needle is escaped for the
|
|
125
|
+
* SQL `LIKE` metacharacters `%`, `_`, and `\`. No index is added — at
|
|
126
|
+
* interactive scale a sequential scan is fine.
|
|
127
|
+
*
|
|
128
|
+
* @param limit Maximum number of rows to return. Must be a positive integer.
|
|
129
|
+
* @param needle Case-insensitive substring to search for.
|
|
130
|
+
* @returns Up to `limit` matching event rows ordered by descending `(created_at, id)`.
|
|
131
|
+
*/
|
|
132
|
+
searchLatest(limit: number, needle: string): Promise<EventRow[]>;
|
|
133
|
+
/**
|
|
134
|
+
* Latest N event rows matching every supplied filter. Drives the
|
|
135
|
+
* agent-facing `event_list` reflection tool, which lets the agent
|
|
136
|
+
* narrow by state, date window, and free-text needle simultaneously
|
|
137
|
+
* — combinations the older {@link listLatest} / {@link searchLatest}
|
|
138
|
+
* pair can't express. Both of those stay as the CLI's call sites.
|
|
139
|
+
*
|
|
140
|
+
* Substring match (when `search` is set) is the same ILIKE-against-
|
|
141
|
+
* `(topic, start_handler, prompt, payload::text)` predicate
|
|
142
|
+
* {@link searchLatest} uses. `dateFrom` is inclusive, `dateTo` is
|
|
143
|
+
* exclusive (`created_at >= $from AND created_at < $to`), so the
|
|
144
|
+
* agent can pass two midnights to get a single local day without
|
|
145
|
+
* worrying about the boundary instant.
|
|
146
|
+
*/
|
|
147
|
+
listFiltered(filter: {
|
|
148
|
+
readonly limit: number;
|
|
149
|
+
readonly search?: string;
|
|
150
|
+
readonly state?: EventState;
|
|
151
|
+
readonly dateFrom?: Date;
|
|
152
|
+
readonly dateTo?: Date;
|
|
153
|
+
}): Promise<EventRow[]>;
|
|
154
|
+
/**
|
|
155
|
+
* Patch one or more fields on an existing event by id. Always bumps
|
|
156
|
+
* `updated_at`. No-op if `patch` contains no recognized fields.
|
|
157
|
+
*/
|
|
158
|
+
update(id: string, patch: EventPatch): Promise<void>;
|
|
159
|
+
/**
|
|
160
|
+
* Block until an event matching `filter` is in the table. Returns
|
|
161
|
+
* the highest-priority such event (FIFO within priority). Subscribes
|
|
162
|
+
* to `LISTEN events_new` on first call so subsequent waits sleep
|
|
163
|
+
* until a NOTIFY arrives instead of polling.
|
|
164
|
+
*
|
|
165
|
+
* **Read-only**: does not mutate the row. For multi-consumer queues,
|
|
166
|
+
* use {@link waitAndClaim} instead, which atomically transitions the
|
|
167
|
+
* row's state on read.
|
|
168
|
+
*
|
|
169
|
+
* @param filter - Topic / state filter; default `{ states: ["pending"] }`.
|
|
170
|
+
* @param signal - Optional AbortSignal to cancel the wait.
|
|
171
|
+
* @throws If aborted via `signal`.
|
|
172
|
+
*/
|
|
173
|
+
waitForNext(filter?: EventFilter, signal?: AbortSignal): Promise<EventRow>;
|
|
174
|
+
/**
|
|
175
|
+
* Atomically transition the highest-priority event in `fromState` to
|
|
176
|
+
* `toState` and return the updated row. Race-safe across multiple
|
|
177
|
+
* concurrent workers via `FOR UPDATE SKIP LOCKED`.
|
|
178
|
+
*
|
|
179
|
+
* Returns `undefined` if no row is in `fromState`. Non-blocking — for
|
|
180
|
+
* blocking semantics, use {@link waitAndClaim}.
|
|
181
|
+
*/
|
|
182
|
+
claim(fromState: EventState, toState: EventState): Promise<EventRow | undefined>;
|
|
183
|
+
/**
|
|
184
|
+
* Block until an event in `fromState` is available, then atomically
|
|
185
|
+
* claim it (transition to `toState`) and return it. Combines
|
|
186
|
+
* {@link claim} with the LISTEN/NOTIFY wait loop used by
|
|
187
|
+
* {@link waitForNext}.
|
|
188
|
+
*
|
|
189
|
+
* @throws If aborted via `signal`.
|
|
190
|
+
*/
|
|
191
|
+
waitAndClaim(fromState: EventState, toState: EventState, signal?: AbortSignal): Promise<EventRow>;
|
|
192
|
+
/** Subscribe this bus's wake-handler to {@link EVENTS_NEW_CHANNEL}. */
|
|
193
|
+
private ensureListening;
|
|
194
|
+
/** Resolve as soon as the next NOTIFY arrives or `signal` aborts. */
|
|
195
|
+
private waitForNotification;
|
|
196
|
+
/** Single SELECT for the highest-priority matching pending event. */
|
|
197
|
+
private queryOne;
|
|
198
|
+
}
|