@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,362 @@
1
+ import { AGENTRUNS_CHANNEL, EVENT_TERMINAL_UPDATE_SQL, SCHEMA_SQL } from "./Schema.js";
2
+ /**
3
+ * Domain client for the `agentruns` table — the assistant's response
4
+ * tree. Each row is one agent invocation that responds to (or descends
5
+ * from) an `events` row.
6
+ *
7
+ * Uses an injected {@link PostgresConnection} for pool + LISTEN access;
8
+ * does not own connection lifecycle.
9
+ *
10
+ * The bus is purely a CRUD surface — no claim queue, no in-process
11
+ * blocking primitive. The {@link import("@getfamiliar/container").AgentrunScheduler}
12
+ * is the sole authority on which row runs next and is guaranteed
13
+ * singleton inside the container, so a postgres-side claim queue
14
+ * (`FOR UPDATE SKIP LOCKED`) would be overkill. The Scheduler reads
15
+ * eligible pending rows via {@link listEligible} and writes lifecycle
16
+ * transitions via {@link update} / {@link settle} / {@link postpone}.
17
+ *
18
+ * The terminal write goes through {@link settle}, which transitions the
19
+ * agentrun to `done` / `failed` *and* recomputes the parent event's
20
+ * terminal state in the same transaction. That keeps the event
21
+ * terminal logic invariant-free — no counter to corrupt — at the cost
22
+ * of one extra query per terminal write.
23
+ */
24
+ export class AgentRunBus {
25
+ connection;
26
+ log;
27
+ constructor(connection, log) {
28
+ this.connection = connection;
29
+ this.log = log;
30
+ }
31
+ /**
32
+ * Apply the events + agentruns schema (idempotent). The same SQL
33
+ * bundle is consumed by {@link EventBus.installSchema}, so calling
34
+ * either is sufficient on daemon start.
35
+ */
36
+ async installSchema() {
37
+ await this.connection.getPool().query(SCHEMA_SQL);
38
+ }
39
+ /**
40
+ * Insert a new agentrun and return the persisted row.
41
+ *
42
+ * @throws If the underlying FK / check constraints reject the row
43
+ * (unknown `eventId`, malformed `topic`, invalid `calltype`).
44
+ */
45
+ async add(run) {
46
+ const result = await this.connection.getPool().query(`INSERT INTO agentruns
47
+ (event_id, parent_agentrun_id, topic, handler, priority, state, prompt, payload, privileged, calltype)
48
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9, $10)
49
+ RETURNING *`, [
50
+ run.eventId,
51
+ run.parentAgentrunId ?? null,
52
+ run.topic,
53
+ run.handler,
54
+ run.priority ?? 50,
55
+ run.state ?? "pending",
56
+ run.prompt ?? null,
57
+ JSON.stringify(run.payload ?? {}),
58
+ run.privileged ?? false,
59
+ run.calltype ?? null,
60
+ ]);
61
+ return mapRow(result.rows[0]);
62
+ }
63
+ /**
64
+ * Patch one or more fields on an existing agentrun by id. Always
65
+ * bumps `updated_at`. **Do not** use this to write a terminal state
66
+ * — use {@link settle} instead so the parent event's terminal state
67
+ * is recomputed in the same transaction.
68
+ */
69
+ async update(id, patch) {
70
+ const sets = ["updated_at = now()"];
71
+ const values = [];
72
+ let n = 1;
73
+ if (patch.state !== undefined) {
74
+ sets.push(`state = $${n++}`);
75
+ values.push(patch.state);
76
+ }
77
+ if (patch.payload !== undefined) {
78
+ sets.push(`payload = $${n++}::jsonb`);
79
+ values.push(JSON.stringify(patch.payload));
80
+ }
81
+ if (patch.result !== undefined) {
82
+ sets.push(`result = $${n++}::jsonb`);
83
+ values.push(JSON.stringify(patch.result));
84
+ }
85
+ if (patch.resultText !== undefined) {
86
+ sets.push(`result_text = $${n++}`);
87
+ values.push(patch.resultText);
88
+ }
89
+ if (patch.error !== undefined) {
90
+ sets.push(`error = $${n++}`);
91
+ values.push(patch.error);
92
+ }
93
+ if (patch.priority !== undefined) {
94
+ sets.push(`priority = $${n++}`);
95
+ values.push(patch.priority);
96
+ }
97
+ if (patch.model !== undefined) {
98
+ sets.push(`model = $${n++}`);
99
+ values.push(patch.model);
100
+ }
101
+ if (patch.systemPrompt !== undefined) {
102
+ sets.push(`system_prompt = $${n++}`);
103
+ values.push(patch.systemPrompt);
104
+ }
105
+ if (patch.initialMessages !== undefined) {
106
+ sets.push(`initial_messages = $${n++}::jsonb`);
107
+ values.push(JSON.stringify(patch.initialMessages));
108
+ }
109
+ if (patch.calltype !== undefined) {
110
+ sets.push(`calltype = $${n++}`);
111
+ values.push(patch.calltype);
112
+ }
113
+ values.push(id);
114
+ const idParam = `$${n}`;
115
+ await this.connection
116
+ .getPool()
117
+ .query(`UPDATE agentruns SET ${sets.join(", ")} WHERE id = ${idParam}`, values);
118
+ }
119
+ /**
120
+ * Terminal write for an agentrun. Transitions the row to `done` or
121
+ * `failed`, optionally recording `result` / `resultText` / `error`,
122
+ * and recomputes the parent event's terminal state in the **same
123
+ * transaction**.
124
+ *
125
+ * The reactive event update is intentionally not a counter: it
126
+ * relies on the `agentruns(event_id)` index to check whether any
127
+ * sibling agentruns are still pending or running.
128
+ */
129
+ async settle(id, toState, outcome = {}) {
130
+ const pool = this.connection.getPool();
131
+ const client = await pool.connect();
132
+ try {
133
+ await client.query("BEGIN");
134
+ const updated = await client.query(`UPDATE agentruns
135
+ SET state = $1,
136
+ result = COALESCE($2::jsonb, result),
137
+ result_text = COALESCE($3, result_text),
138
+ error = COALESCE($4, error),
139
+ updated_at = now()
140
+ WHERE id = $5
141
+ RETURNING event_id`, [
142
+ toState,
143
+ outcome.result === undefined ? null : JSON.stringify(outcome.result),
144
+ outcome.resultText ?? null,
145
+ outcome.error ?? null,
146
+ id,
147
+ ]);
148
+ if (updated.rows.length === 0) {
149
+ await client.query("ROLLBACK");
150
+ throw new Error(`Agentrun ${id} not found`);
151
+ }
152
+ const eventId = updated.rows[0].event_id;
153
+ await client.query(EVENT_TERMINAL_UPDATE_SQL, [eventId, id, toState]);
154
+ await client.query("COMMIT");
155
+ }
156
+ catch (err) {
157
+ await client.query("ROLLBACK").catch(() => { });
158
+ throw err;
159
+ }
160
+ finally {
161
+ client.release();
162
+ }
163
+ }
164
+ /**
165
+ * Fetch one agentrun by id, or `undefined` when missing.
166
+ */
167
+ async getById(id) {
168
+ const result = await this.connection
169
+ .getPool()
170
+ .query(`SELECT * FROM agentruns WHERE id = $1`, [id]);
171
+ return result.rows.length > 0 ? mapRow(result.rows[0]) : undefined;
172
+ }
173
+ /**
174
+ * Fetch every agentrun in `state='pending'` whose `not_before` window
175
+ * has opened, in dispatch order (`priority desc, id asc`). The
176
+ * {@link AgentrunScheduler} calls this once per scheduling pass.
177
+ *
178
+ * Returns an empty array when nothing is ready. The Scheduler then
179
+ * waits on its NOTIFY subscription or a parked `not_before` timer
180
+ * before re-checking.
181
+ */
182
+ async listEligible() {
183
+ const result = await this.connection.getPool().query(`SELECT * FROM agentruns
184
+ WHERE state = 'pending'
185
+ AND (not_before IS NULL OR not_before <= now())
186
+ ORDER BY priority DESC, id ASC`);
187
+ return result.rows.map(mapRow);
188
+ }
189
+ /**
190
+ * `true` when every `calltype='started'` child of `parentId` is in
191
+ * a terminal state (`done` / `failed`). Used by the Scheduler
192
+ * after a started child settles, to decide whether the parent can
193
+ * leave `waiting` and go back to `pending`.
194
+ *
195
+ * Returns `true` when there are no started children at all (the
196
+ * trivially-vacuous case); the Scheduler still owns the "should
197
+ * we actually re-pend this parent?" decision and only invokes
198
+ * this method when at least one started child has just settled.
199
+ */
200
+ async areAllStartedChildrenSettled(parentId) {
201
+ const result = await this.connection.getPool().query(`SELECT EXISTS (
202
+ SELECT 1 FROM agentruns
203
+ WHERE parent_agentrun_id = $1
204
+ AND calltype = 'started'
205
+ AND state NOT IN ('done','failed')
206
+ ) AS exists`, [parentId]);
207
+ return result.rows[0]?.exists === false;
208
+ }
209
+ /**
210
+ * Fetch all agentrun rows whose `updated_at` is `>= since`, ordered
211
+ * by `(updated_at, id)`. Used by the polling-based report layer to
212
+ * pick up new agentruns and state transitions without NOTIFY.
213
+ */
214
+ async listSince(since) {
215
+ const result = await this.connection.getPool().query(`SELECT * FROM agentruns
216
+ WHERE updated_at >= $1
217
+ ORDER BY updated_at, id`, [since]);
218
+ return result.rows.map(mapRow);
219
+ }
220
+ /**
221
+ * Fetch all agentruns belonging to one event, ordered by id.
222
+ * Used by the report layer to aggregate token totals and runtime
223
+ * across an event's full agentrun tree at finalization time.
224
+ */
225
+ async listByEventId(eventId) {
226
+ const result = await this.connection.getPool().query(`SELECT * FROM agentruns
227
+ WHERE event_id = $1
228
+ ORDER BY id`, [eventId]);
229
+ return result.rows.map(mapRow);
230
+ }
231
+ /**
232
+ * Postpone an agentrun after a retryable inference error.
233
+ * Returns the row to `pending`, bumps `retry_count`, sets
234
+ * `not_before` to the supplied timestamp, and records the latest
235
+ * attempt's error text. The existing `agentruns_changed` UPDATE
236
+ * trigger fires on the state write so a NOTIFY-subscribed
237
+ * Scheduler wakes up — it just won't include the row in
238
+ * {@link listEligible} until `not_before` passes.
239
+ *
240
+ * Throws if the row is missing (mirrors {@link settle}).
241
+ */
242
+ async postpone(id, runAfter, errorText) {
243
+ const result = await this.connection.getPool().query(`UPDATE agentruns
244
+ SET state = 'pending',
245
+ retry_count = retry_count + 1,
246
+ not_before = $2,
247
+ error = $3,
248
+ updated_at = now()
249
+ WHERE id = $1`, [id, runAfter, errorText]);
250
+ if (result.rowCount === 0) {
251
+ throw new Error(`Agentrun ${id} not found`);
252
+ }
253
+ }
254
+ /**
255
+ * Subscribe to {@link AGENTRUNS_CHANNEL}. The notification payload
256
+ * (`<event_id>:<id>`) is parsed, the row is fetched by id, and
257
+ * `handler` is invoked with it. Fires on both INSERT and state
258
+ * UPDATE — callers that only care about one transition must filter
259
+ * on `row.state` themselves.
260
+ *
261
+ * Errors thrown by `handler` are caught and logged so one bad
262
+ * subscriber doesn't break others on the same channel.
263
+ *
264
+ * Returns a disposer; call it to unlisten. Idempotent.
265
+ *
266
+ * The {@link AgentrunScheduler} subscribes via
267
+ * {@link PostgresConnection.listen} directly (it only needs a
268
+ * "something changed" wake hint, not full row fetches); this
269
+ * helper exists for host-side consumers like
270
+ * `HostContext.events.emit`'s `onAgentRun` callback.
271
+ */
272
+ async listen(handler) {
273
+ const wrapper = (payload) => {
274
+ this.log?.debug({ channel: AGENTRUNS_CHANNEL, payload }, "NOTIFY agentruns_changed");
275
+ void this.dispatchNotification(payload, handler);
276
+ };
277
+ await this.connection.listen(AGENTRUNS_CHANNEL, wrapper);
278
+ let disposed = false;
279
+ return async () => {
280
+ if (disposed) {
281
+ return;
282
+ }
283
+ disposed = true;
284
+ await this.connection.unlisten(AGENTRUNS_CHANNEL, wrapper);
285
+ };
286
+ }
287
+ /**
288
+ * Inner notification dispatcher for {@link listen}. Parses the
289
+ * channel payload (`<event_id>:<id>`), fetches the row by id, and
290
+ * invokes the handler — catching and logging any error from the
291
+ * handler itself.
292
+ */
293
+ async dispatchNotification(payload, handler) {
294
+ // Payload format: `<event_id>:<agent_run_id>` (see Schema.ts
295
+ // `agentruns_notify_changed`). A malformed payload is a
296
+ // strong signal that the deployed trigger function is out of
297
+ // sync with the source — surface it loudly instead of
298
+ // silently dropping notifications and leaving the caller's
299
+ // spinner stuck forever.
300
+ const colon = payload.indexOf(":");
301
+ if (colon < 0) {
302
+ const warning = `AgentRunBus: malformed ${AGENTRUNS_CHANNEL} payload (no colon): "${payload}" — is the agentruns_notify_changed trigger function out of date? Restart the daemon to reinstall the schema.`;
303
+ if (this.log) {
304
+ this.log.warn({ payload }, warning);
305
+ }
306
+ else {
307
+ console.warn(warning);
308
+ }
309
+ return;
310
+ }
311
+ const id = payload.slice(colon + 1);
312
+ try {
313
+ const row = await this.getById(id);
314
+ if (!row) {
315
+ const warning = `AgentRunBus: no agentrun found for id="${id}" (payload="${payload}") — stale or malformed NOTIFY payload?`;
316
+ if (this.log) {
317
+ this.log.warn({ id, payload }, warning);
318
+ }
319
+ else {
320
+ console.warn(warning);
321
+ }
322
+ return;
323
+ }
324
+ await handler(row);
325
+ }
326
+ catch (err) {
327
+ this.log?.error({ id, err: err instanceof Error ? err.message : String(err) }, "AgentRunBus listen handler error");
328
+ }
329
+ }
330
+ }
331
+ /**
332
+ * Convert a snake_case raw row into the camelCase {@link AgentRunRow}.
333
+ * `state` is trusted to be a known {@link AgentRunState} (we control
334
+ * every writer).
335
+ */
336
+ function mapRow(raw) {
337
+ return {
338
+ id: raw.id,
339
+ eventId: raw.event_id,
340
+ parentAgentrunId: raw.parent_agentrun_id,
341
+ topic: raw.topic,
342
+ handler: raw.handler,
343
+ priority: raw.priority,
344
+ state: raw.state,
345
+ prompt: raw.prompt,
346
+ payload: raw.payload,
347
+ result: raw.result,
348
+ resultText: raw.result_text,
349
+ error: raw.error,
350
+ privileged: raw.privileged,
351
+ calltype: raw.calltype === "scheduled" || raw.calltype === "started"
352
+ ? raw.calltype
353
+ : null,
354
+ retryCount: raw.retry_count,
355
+ notBefore: raw.not_before,
356
+ model: raw.model,
357
+ systemPrompt: raw.system_prompt,
358
+ initialMessages: raw.initial_messages,
359
+ createdAt: raw.created_at,
360
+ updatedAt: raw.updated_at,
361
+ };
362
+ }