@electric-ax/agents-server 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/LICENSE +177 -0
  2. package/dist/chunk-Cl8Af3a2.js +11 -0
  3. package/dist/entrypoint.js +7319 -0
  4. package/dist/index.cjs +7090 -0
  5. package/dist/index.d.cts +4262 -0
  6. package/dist/index.d.ts +4263 -0
  7. package/dist/index.js +7053 -0
  8. package/drizzle/0000_baseline.sql +97 -0
  9. package/drizzle/0001_entity_tags_and_bridges.sql +45 -0
  10. package/drizzle/0002_tag_outbox_hardening.sql +14 -0
  11. package/drizzle/0003_entity_manifest_sources.sql +11 -0
  12. package/drizzle/0004_tenant_scoping.sql +139 -0
  13. package/drizzle/0005_pull_wake_control_plane.sql +156 -0
  14. package/drizzle/meta/0000_snapshot.json +593 -0
  15. package/drizzle/meta/_journal.json +48 -0
  16. package/package.json +89 -0
  17. package/src/authenticated-user-format.ts +17 -0
  18. package/src/claim-write-token-store.ts +74 -0
  19. package/src/db/index.ts +53 -0
  20. package/src/db/schema.ts +490 -0
  21. package/src/dev-asserted-auth.ts +46 -0
  22. package/src/dispatch-policy-schema.ts +52 -0
  23. package/src/electric-agents/adapter-types.ts +70 -0
  24. package/src/electric-agents/default-entity-schemas.ts +1 -0
  25. package/src/electric-agents/schema-validator.ts +143 -0
  26. package/src/electric-agents-http.ts +46 -0
  27. package/src/electric-agents-types.ts +335 -0
  28. package/src/entity-bridge-manager.ts +694 -0
  29. package/src/entity-manager.ts +2601 -0
  30. package/src/entity-projector.ts +765 -0
  31. package/src/entity-registry.ts +1162 -0
  32. package/src/entrypoint-lib.ts +295 -0
  33. package/src/entrypoint.ts +11 -0
  34. package/src/host.ts +323 -0
  35. package/src/index.ts +49 -0
  36. package/src/manifest-side-effects.ts +183 -0
  37. package/src/routing/agent-ui-router.ts +81 -0
  38. package/src/routing/context.ts +35 -0
  39. package/src/routing/cron-router.ts +45 -0
  40. package/src/routing/dispatch-policy.ts +248 -0
  41. package/src/routing/durable-streams-router.ts +407 -0
  42. package/src/routing/durable-streams-routing-adapter.ts +96 -0
  43. package/src/routing/electric-proxy-router.ts +61 -0
  44. package/src/routing/entities-router.ts +484 -0
  45. package/src/routing/entity-types-router.ts +229 -0
  46. package/src/routing/global-router.ts +33 -0
  47. package/src/routing/hooks.ts +123 -0
  48. package/src/routing/internal-router.ts +741 -0
  49. package/src/routing/oss-server-router.ts +56 -0
  50. package/src/routing/runners-router.ts +416 -0
  51. package/src/routing/schema.ts +141 -0
  52. package/src/routing/stream-append.ts +196 -0
  53. package/src/routing/tenant-stream-paths.ts +26 -0
  54. package/src/runtime-registry.ts +49 -0
  55. package/src/runtime.ts +537 -0
  56. package/src/scheduler.ts +788 -0
  57. package/src/schema-validation.ts +15 -0
  58. package/src/server.ts +374 -0
  59. package/src/standalone-runtime.ts +188 -0
  60. package/src/stream-client.ts +842 -0
  61. package/src/tag-stream-outbox-drainer.ts +188 -0
  62. package/src/tenant.ts +25 -0
  63. package/src/tracing.ts +57 -0
  64. package/src/utils/electric-url.ts +15 -0
  65. package/src/utils/log.ts +95 -0
  66. package/src/utils/server-utils.ts +245 -0
  67. package/src/utils/webhook-url.ts +33 -0
  68. package/src/wake-registry.ts +946 -0
@@ -0,0 +1,490 @@
1
+ import { sql } from 'drizzle-orm'
2
+ import {
3
+ bigint,
4
+ bigserial,
5
+ boolean,
6
+ check,
7
+ index,
8
+ integer,
9
+ jsonb,
10
+ pgTable,
11
+ primaryKey,
12
+ serial,
13
+ text,
14
+ timestamp,
15
+ unique,
16
+ } from 'drizzle-orm/pg-core'
17
+
18
+ export const entityTypes = pgTable(
19
+ `entity_types`,
20
+ {
21
+ tenantId: text(`tenant_id`).notNull().default(`default`),
22
+ name: text(`name`).notNull(),
23
+ description: text(`description`).notNull(),
24
+ creationSchema: jsonb(`creation_schema`),
25
+ inboxSchemas: jsonb(`inbox_schemas`),
26
+ stateSchemas: jsonb(`state_schemas`),
27
+ serveEndpoint: text(`serve_endpoint`),
28
+ defaultDispatchPolicy: jsonb(`default_dispatch_policy`),
29
+ revision: integer(`revision`).notNull().default(1),
30
+ createdAt: text(`created_at`).notNull(),
31
+ updatedAt: text(`updated_at`).notNull(),
32
+ },
33
+ (table) => [primaryKey({ columns: [table.tenantId, table.name] })]
34
+ )
35
+
36
+ export const entities = pgTable(
37
+ `entities`,
38
+ {
39
+ tenantId: text(`tenant_id`).notNull().default(`default`),
40
+ url: text(`url`).notNull(),
41
+ type: text(`type`).notNull(),
42
+ status: text(`status`).notNull().default(`idle`),
43
+ subscriptionId: text(`subscription_id`).notNull(),
44
+ dispatchPolicy: jsonb(`dispatch_policy`),
45
+ writeToken: text(`write_token`).notNull(),
46
+ tags: jsonb(`tags`).notNull().default({}),
47
+ tagsIndex: text(`tags_index`)
48
+ .array()
49
+ .notNull()
50
+ .default(sql`'{}'::text[]`),
51
+ spawnArgs: jsonb(`spawn_args`).default({}),
52
+ parent: text(`parent`),
53
+ typeRevision: integer(`type_revision`),
54
+ inboxSchemas: jsonb(`inbox_schemas`),
55
+ stateSchemas: jsonb(`state_schemas`),
56
+ createdAt: bigint(`created_at`, { mode: `number` }).notNull(),
57
+ updatedAt: bigint(`updated_at`, { mode: `number` }).notNull(),
58
+ },
59
+ (table) => [
60
+ primaryKey({ columns: [table.tenantId, table.url] }),
61
+ index(`idx_entities_type`).on(table.tenantId, table.type),
62
+ index(`idx_entities_status`).on(table.tenantId, table.status),
63
+ index(`idx_entities_parent`).on(table.tenantId, table.parent),
64
+ index(`entities_tags_index_gin`).using(`gin`, table.tagsIndex),
65
+ check(
66
+ `chk_entities_status`,
67
+ sql`${table.status} IN ('spawning', 'running', 'idle', 'stopped')`
68
+ ),
69
+ ]
70
+ )
71
+
72
+ export const users = pgTable(
73
+ `users`,
74
+ {
75
+ tenantId: text(`tenant_id`).notNull().default(`default`),
76
+ id: text(`id`).notNull(),
77
+ displayName: text(`display_name`),
78
+ email: text(`email`),
79
+ avatarUrl: text(`avatar_url`),
80
+ authProvider: text(`auth_provider`),
81
+ authSubject: text(`auth_subject`),
82
+ profile: jsonb(`profile`).notNull().default({}),
83
+ metadata: jsonb(`metadata`).notNull().default({}),
84
+ createdAt: timestamp(`created_at`, { withTimezone: true })
85
+ .notNull()
86
+ .defaultNow(),
87
+ updatedAt: timestamp(`updated_at`, { withTimezone: true })
88
+ .notNull()
89
+ .defaultNow(),
90
+ },
91
+ (table) => [
92
+ primaryKey({ columns: [table.tenantId, table.id] }),
93
+ index(`idx_users_email`).on(table.tenantId, table.email),
94
+ index(`idx_users_auth_identity`).on(
95
+ table.tenantId,
96
+ table.authProvider,
97
+ table.authSubject
98
+ ),
99
+ ]
100
+ )
101
+
102
+ export const runners = pgTable(
103
+ `runners`,
104
+ {
105
+ tenantId: text(`tenant_id`).notNull().default(`default`),
106
+ id: text(`id`).notNull(),
107
+ ownerUserId: text(`owner_user_id`).notNull(),
108
+ label: text(`label`).notNull(),
109
+ kind: text(`kind`).notNull().default(`local`),
110
+ adminStatus: text(`admin_status`).notNull().default(`enabled`),
111
+ wakeStream: text(`wake_stream`).notNull(),
112
+ wakeStreamOffset: text(`wake_stream_offset`),
113
+ lastSeenAt: timestamp(`last_seen_at`, { withTimezone: true }),
114
+ livenessLeaseExpiresAt: timestamp(`liveness_lease_expires_at`, {
115
+ withTimezone: true,
116
+ }),
117
+ createdAt: timestamp(`created_at`, { withTimezone: true })
118
+ .notNull()
119
+ .defaultNow(),
120
+ updatedAt: timestamp(`updated_at`, { withTimezone: true })
121
+ .notNull()
122
+ .defaultNow(),
123
+ },
124
+ (table) => [
125
+ primaryKey({ columns: [table.tenantId, table.id] }),
126
+ unique(`uq_runners_wake_stream`).on(table.tenantId, table.wakeStream),
127
+ index(`idx_runners_owner_user_id`).on(table.tenantId, table.ownerUserId),
128
+ index(`idx_runners_admin_status`).on(table.tenantId, table.adminStatus),
129
+ index(`idx_runners_liveness_lease_expires_at`).on(
130
+ table.tenantId,
131
+ table.livenessLeaseExpiresAt
132
+ ),
133
+ check(
134
+ `chk_runners_kind`,
135
+ sql`${table.kind} IN ('local', 'cloud-worker', 'sandbox', 'ci', 'server')`
136
+ ),
137
+ check(
138
+ `chk_runners_admin_status`,
139
+ sql`${table.adminStatus} IN ('enabled', 'disabled')`
140
+ ),
141
+ ]
142
+ )
143
+
144
+ export const entityDispatchState = pgTable(
145
+ `entity_dispatch_state`,
146
+ {
147
+ tenantId: text(`tenant_id`).notNull().default(`default`),
148
+ entityUrl: text(`entity_url`).notNull(),
149
+ pendingSourceStreams: jsonb(`pending_source_streams`).notNull().default([]),
150
+ pendingReason: text(`pending_reason`),
151
+ pendingSince: timestamp(`pending_since`, { withTimezone: true }),
152
+ outstandingWakeId: text(`outstanding_wake_id`),
153
+ outstandingWakeTarget: jsonb(`outstanding_wake_target`),
154
+ outstandingWakeCreatedAt: timestamp(`outstanding_wake_created_at`, {
155
+ withTimezone: true,
156
+ }),
157
+ activeConsumerId: text(`active_consumer_id`),
158
+ activeRunnerId: text(`active_runner_id`),
159
+ activeEpoch: integer(`active_epoch`),
160
+ activeClaimedAt: timestamp(`active_claimed_at`, { withTimezone: true }),
161
+ activeLeaseExpiresAt: timestamp(`active_lease_expires_at`, {
162
+ withTimezone: true,
163
+ }),
164
+ lastWakeId: text(`last_wake_id`),
165
+ lastClaimedAt: timestamp(`last_claimed_at`, { withTimezone: true }),
166
+ lastReleasedAt: timestamp(`last_released_at`, { withTimezone: true }),
167
+ lastCompletedAt: timestamp(`last_completed_at`, { withTimezone: true }),
168
+ lastError: text(`last_error`),
169
+ updatedAt: timestamp(`updated_at`, { withTimezone: true })
170
+ .notNull()
171
+ .defaultNow(),
172
+ },
173
+ (table) => [
174
+ primaryKey({ columns: [table.tenantId, table.entityUrl] }),
175
+ index(`idx_entity_dispatch_state_active_runner`).on(
176
+ table.tenantId,
177
+ table.activeRunnerId
178
+ ),
179
+ index(`idx_entity_dispatch_state_outstanding_wake`).on(
180
+ table.tenantId,
181
+ table.outstandingWakeId
182
+ ),
183
+ index(`idx_entity_dispatch_state_active_lease`).on(
184
+ table.tenantId,
185
+ table.activeLeaseExpiresAt
186
+ ),
187
+ ]
188
+ )
189
+
190
+ export const wakeNotifications = pgTable(
191
+ `wake_notifications`,
192
+ {
193
+ tenantId: text(`tenant_id`).notNull().default(`default`),
194
+ wakeId: text(`wake_id`).notNull(),
195
+ entityUrl: text(`entity_url`).notNull(),
196
+ targetType: text(`target_type`).notNull(),
197
+ targetRunnerId: text(`target_runner_id`),
198
+ targetWebhookUrl: text(`target_webhook_url`),
199
+ targetWorkerPoolId: text(`target_worker_pool_id`),
200
+ runnerWakeStream: text(`runner_wake_stream`),
201
+ runnerWakeStreamOffset: text(`runner_wake_stream_offset`),
202
+ notificationPublic: jsonb(`notification_public`).notNull(),
203
+ deliveryStatus: text(`delivery_status`).notNull().default(`queued`),
204
+ claimStatus: text(`claim_status`).notNull().default(`unclaimed`),
205
+ createdAt: timestamp(`created_at`, { withTimezone: true })
206
+ .notNull()
207
+ .defaultNow(),
208
+ deliveredAt: timestamp(`delivered_at`, { withTimezone: true }),
209
+ claimedAt: timestamp(`claimed_at`, { withTimezone: true }),
210
+ resolvedAt: timestamp(`resolved_at`, { withTimezone: true }),
211
+ },
212
+ (table) => [
213
+ primaryKey({ columns: [table.tenantId, table.wakeId] }),
214
+ index(`idx_wake_notifications_entity_url`).on(
215
+ table.tenantId,
216
+ table.entityUrl
217
+ ),
218
+ index(`idx_wake_notifications_target_runner`).on(
219
+ table.tenantId,
220
+ table.targetRunnerId
221
+ ),
222
+ index(`idx_wake_notifications_delivery_status`).on(
223
+ table.tenantId,
224
+ table.deliveryStatus
225
+ ),
226
+ index(`idx_wake_notifications_claim_status`).on(
227
+ table.tenantId,
228
+ table.claimStatus
229
+ ),
230
+ index(`idx_wake_notifications_created_at`).on(
231
+ table.tenantId,
232
+ table.createdAt
233
+ ),
234
+ check(
235
+ `chk_wake_notifications_target_type`,
236
+ sql`${table.targetType} IN ('webhook', 'runner', 'worker-pool')`
237
+ ),
238
+ check(
239
+ `chk_wake_notifications_delivery_status`,
240
+ sql`${table.deliveryStatus} IN ('queued', 'delivered', 'failed', 'superseded')`
241
+ ),
242
+ check(
243
+ `chk_wake_notifications_claim_status`,
244
+ sql`${table.claimStatus} IN ('unclaimed', 'claimed', 'completed', 'expired')`
245
+ ),
246
+ ]
247
+ )
248
+
249
+ export const consumerClaims = pgTable(
250
+ `consumer_claims`,
251
+ {
252
+ tenantId: text(`tenant_id`).notNull().default(`default`),
253
+ consumerId: text(`consumer_id`).notNull(),
254
+ epoch: integer(`epoch`).notNull(),
255
+ wakeId: text(`wake_id`),
256
+ entityUrl: text(`entity_url`).notNull(),
257
+ streamPath: text(`stream_path`).notNull(),
258
+ runnerId: text(`runner_id`),
259
+ status: text(`status`).notNull().default(`active`),
260
+ claimedAt: timestamp(`claimed_at`, { withTimezone: true })
261
+ .notNull()
262
+ .defaultNow(),
263
+ lastHeartbeatAt: timestamp(`last_heartbeat_at`, { withTimezone: true }),
264
+ leaseExpiresAt: timestamp(`lease_expires_at`, { withTimezone: true }),
265
+ releasedAt: timestamp(`released_at`, { withTimezone: true }),
266
+ ackedStreams: jsonb(`acked_streams`),
267
+ updatedAt: timestamp(`updated_at`, { withTimezone: true })
268
+ .notNull()
269
+ .defaultNow(),
270
+ },
271
+ (table) => [
272
+ primaryKey({ columns: [table.tenantId, table.consumerId, table.epoch] }),
273
+ index(`idx_consumer_claims_entity_status`).on(
274
+ table.tenantId,
275
+ table.entityUrl,
276
+ table.status
277
+ ),
278
+ index(`idx_consumer_claims_runner`).on(table.tenantId, table.runnerId),
279
+ index(`idx_consumer_claims_wake_id`).on(table.tenantId, table.wakeId),
280
+ index(`idx_consumer_claims_lease_expires_at`).on(
281
+ table.tenantId,
282
+ table.leaseExpiresAt
283
+ ),
284
+ check(
285
+ `chk_consumer_claims_status`,
286
+ sql`${table.status} IN ('active', 'released', 'expired', 'failed')`
287
+ ),
288
+ ]
289
+ )
290
+
291
+ export const wakeRegistrations = pgTable(
292
+ `wake_registrations`,
293
+ {
294
+ id: serial(`id`).primaryKey(),
295
+ tenantId: text(`tenant_id`).notNull().default(`default`),
296
+ subscriberUrl: text(`subscriber_url`).notNull(),
297
+ sourceUrl: text(`source_url`).notNull(),
298
+ condition: jsonb(`condition`).notNull(),
299
+ debounceMs: integer(`debounce_ms`).notNull().default(0),
300
+ timeoutMs: integer(`timeout_ms`).notNull().default(0),
301
+ oneShot: boolean(`one_shot`).notNull().default(false),
302
+ timeoutConsumed: boolean(`timeout_consumed`).notNull().default(false),
303
+ includeResponse: boolean(`include_response`).notNull().default(true),
304
+ manifestKey: text(`manifest_key`),
305
+ createdAt: timestamp(`created_at`, { withTimezone: true })
306
+ .notNull()
307
+ .defaultNow(),
308
+ },
309
+ (table) => [
310
+ index(`idx_wake_source_url`).on(table.tenantId, table.sourceUrl),
311
+ unique(`uq_wake_registration`).on(
312
+ table.tenantId,
313
+ table.subscriberUrl,
314
+ table.sourceUrl,
315
+ table.oneShot,
316
+ table.debounceMs,
317
+ table.timeoutMs,
318
+ table.condition,
319
+ table.manifestKey
320
+ ),
321
+ ]
322
+ )
323
+
324
+ export const subscriptionWebhooks = pgTable(
325
+ `subscription_webhooks`,
326
+ {
327
+ tenantId: text(`tenant_id`).notNull().default(`default`),
328
+ subscriptionId: text(`subscription_id`).notNull(),
329
+ webhookUrl: text(`webhook_url`).notNull(),
330
+ createdAt: timestamp(`created_at`, { withTimezone: true })
331
+ .notNull()
332
+ .defaultNow(),
333
+ },
334
+ (table) => [primaryKey({ columns: [table.tenantId, table.subscriptionId] })]
335
+ )
336
+
337
+ export const consumerCallbacks = pgTable(
338
+ `consumer_callbacks`,
339
+ {
340
+ tenantId: text(`tenant_id`).notNull().default(`default`),
341
+ consumerId: text(`consumer_id`).notNull(),
342
+ callbackUrl: text(`callback_url`).notNull(),
343
+ primaryStream: text(`primary_stream`),
344
+ createdAt: timestamp(`created_at`, { withTimezone: true })
345
+ .notNull()
346
+ .defaultNow(),
347
+ },
348
+ (table) => [
349
+ primaryKey({ columns: [table.tenantId, table.consumerId] }),
350
+ index(`idx_consumer_callbacks_primary_stream`).on(
351
+ table.tenantId,
352
+ table.primaryStream
353
+ ),
354
+ ]
355
+ )
356
+
357
+ export const scheduledTasks = pgTable(
358
+ `scheduled_tasks`,
359
+ {
360
+ id: bigserial(`id`, { mode: `number` }).primaryKey(),
361
+ tenantId: text(`tenant_id`).notNull().default(`default`),
362
+ kind: text(`kind`).notNull(),
363
+ payload: jsonb(`payload`).notNull(),
364
+ fireAt: timestamp(`fire_at`, { withTimezone: true }).notNull(),
365
+ cronExpression: text(`cron_expression`),
366
+ cronTimezone: text(`cron_timezone`),
367
+ cronTickNumber: integer(`cron_tick_number`),
368
+ ownerEntityUrl: text(`owner_entity_url`),
369
+ manifestKey: text(`manifest_key`),
370
+ claimedBy: text(`claimed_by`),
371
+ claimedAt: timestamp(`claimed_at`, { withTimezone: true }),
372
+ completedAt: timestamp(`completed_at`, { withTimezone: true }),
373
+ lastError: text(`last_error`),
374
+ createdAt: timestamp(`created_at`, { withTimezone: true })
375
+ .notNull()
376
+ .defaultNow(),
377
+ },
378
+ (table) => [
379
+ check(
380
+ `chk_scheduled_tasks_kind`,
381
+ sql`${table.kind} IN ('delayed_send', 'cron_tick')`
382
+ ),
383
+ index(`idx_scheduled_tasks_fire_ready`)
384
+ .on(table.tenantId, table.fireAt)
385
+ .where(sql`${table.completedAt} IS NULL AND ${table.claimedAt} IS NULL`),
386
+ unique(`uq_cron_tick`).on(
387
+ table.tenantId,
388
+ table.cronExpression,
389
+ table.cronTimezone,
390
+ table.cronTickNumber
391
+ ),
392
+ index(`idx_scheduled_tasks_manifest_pending`)
393
+ .on(table.tenantId, table.ownerEntityUrl, table.manifestKey)
394
+ .where(
395
+ sql`${table.kind} = 'delayed_send' AND ${table.completedAt} IS NULL AND ${table.manifestKey} IS NOT NULL`
396
+ ),
397
+ index(`idx_scheduled_tasks_stale_claims`)
398
+ .on(table.tenantId, table.claimedAt)
399
+ .where(
400
+ sql`${table.completedAt} IS NULL AND ${table.claimedAt} IS NOT NULL`
401
+ ),
402
+ ]
403
+ )
404
+
405
+ export const entityBridges = pgTable(
406
+ `entity_bridges`,
407
+ {
408
+ tenantId: text(`tenant_id`).notNull().default(`default`),
409
+ sourceRef: text(`source_ref`).notNull(),
410
+ tags: jsonb(`tags`).notNull(),
411
+ streamUrl: text(`stream_url`).notNull(),
412
+ shapeHandle: text(`shape_handle`),
413
+ shapeOffset: text(`shape_offset`),
414
+ lastObserverActivityAt: timestamp(`last_observer_activity_at`, {
415
+ withTimezone: true,
416
+ })
417
+ .notNull()
418
+ .defaultNow(),
419
+ createdAt: timestamp(`created_at`, { withTimezone: true })
420
+ .notNull()
421
+ .defaultNow(),
422
+ updatedAt: timestamp(`updated_at`, { withTimezone: true })
423
+ .notNull()
424
+ .defaultNow(),
425
+ },
426
+ (table) => [
427
+ primaryKey({ columns: [table.tenantId, table.sourceRef] }),
428
+ unique(`uq_entity_bridges_stream_url`).on(table.tenantId, table.streamUrl),
429
+ ]
430
+ )
431
+
432
+ export const entityManifestSources = pgTable(
433
+ `entity_manifest_sources`,
434
+ {
435
+ tenantId: text(`tenant_id`).notNull().default(`default`),
436
+ ownerEntityUrl: text(`owner_entity_url`).notNull(),
437
+ manifestKey: text(`manifest_key`).notNull(),
438
+ sourceRef: text(`source_ref`).notNull(),
439
+ createdAt: timestamp(`created_at`, { withTimezone: true })
440
+ .notNull()
441
+ .defaultNow(),
442
+ updatedAt: timestamp(`updated_at`, { withTimezone: true })
443
+ .notNull()
444
+ .defaultNow(),
445
+ },
446
+ (table) => [
447
+ unique(`uq_entity_manifest_source`).on(
448
+ table.tenantId,
449
+ table.ownerEntityUrl,
450
+ table.manifestKey
451
+ ),
452
+ index(`idx_entity_manifest_sources_source_ref`).on(
453
+ table.tenantId,
454
+ table.sourceRef
455
+ ),
456
+ ]
457
+ )
458
+
459
+ export const tagStreamOutbox = pgTable(
460
+ `tag_stream_outbox`,
461
+ {
462
+ id: bigserial(`id`, { mode: `number` }).primaryKey(),
463
+ tenantId: text(`tenant_id`).notNull().default(`default`),
464
+ entityUrl: text(`entity_url`).notNull(),
465
+ collection: text(`collection`).notNull(),
466
+ op: text(`op`).notNull(),
467
+ key: text(`key`).notNull(),
468
+ rowData: jsonb(`row_data`),
469
+ attemptCount: integer(`attempt_count`).notNull().default(0),
470
+ lastError: text(`last_error`),
471
+ claimedBy: text(`claimed_by`),
472
+ claimedAt: timestamp(`claimed_at`, { withTimezone: true }),
473
+ deadLetteredAt: timestamp(`dead_lettered_at`, { withTimezone: true }),
474
+ createdAt: timestamp(`created_at`, { withTimezone: true })
475
+ .notNull()
476
+ .defaultNow(),
477
+ },
478
+ (table) => [
479
+ index(`idx_tag_stream_outbox_unclaimed`)
480
+ .on(table.tenantId, table.createdAt)
481
+ .where(
482
+ sql`${table.claimedAt} IS NULL AND ${table.deadLetteredAt} IS NULL`
483
+ ),
484
+ index(`idx_tag_stream_outbox_stale_claims`)
485
+ .on(table.tenantId, table.claimedAt)
486
+ .where(
487
+ sql`${table.claimedAt} IS NOT NULL AND ${table.deadLetteredAt} IS NULL`
488
+ ),
489
+ ]
490
+ )
@@ -0,0 +1,46 @@
1
+ import type {
2
+ AuthenticateRequest,
3
+ AuthenticatedRequestUser,
4
+ } from './electric-agents-types.js'
5
+
6
+ export interface DevAssertedAuthOptions {
7
+ enabled?: boolean
8
+ defaultEmail?: string
9
+ defaultName?: string
10
+ }
11
+
12
+ export const DEV_ASSERTED_EMAIL_HEADER = `x-electric-asserted-email`
13
+ export const DEV_ASSERTED_NAME_HEADER = `x-electric-asserted-name`
14
+
15
+ function clean(value: string | undefined | null): string | undefined {
16
+ const trimmed = value?.trim()
17
+ return trimmed || undefined
18
+ }
19
+
20
+ export function createDevAssertedAuthenticateRequest(
21
+ options: DevAssertedAuthOptions
22
+ ): AuthenticateRequest | undefined {
23
+ if (!options.enabled) return undefined
24
+
25
+ return (request): AuthenticatedRequestUser | null => {
26
+ const email =
27
+ clean(request.headers.get(DEV_ASSERTED_EMAIL_HEADER)) ??
28
+ clean(options.defaultEmail)
29
+ const name =
30
+ clean(request.headers.get(DEV_ASSERTED_NAME_HEADER)) ??
31
+ clean(options.defaultName)
32
+ const userId = email ?? name
33
+ if (!userId) return null
34
+ return { userId, email, name }
35
+ }
36
+ }
37
+
38
+ export function devAssertedAuthOptionsFromEnv(
39
+ env: Record<string, string | undefined> = process.env
40
+ ): DevAssertedAuthOptions {
41
+ return {
42
+ enabled: env.ELECTRIC_AGENTS_DEV_ASSERTED_AUTH === `1`,
43
+ defaultEmail: env.ELECTRIC_ASSERTED_AUTH_EMAIL,
44
+ defaultName: env.ELECTRIC_ASSERTED_AUTH_NAME,
45
+ }
46
+ }
@@ -0,0 +1,52 @@
1
+ import { Type } from '@sinclair/typebox'
2
+ import { schemaValidator } from './schema-validation.js'
3
+ import type { DispatchPolicy } from './electric-agents-types.js'
4
+
5
+ const nonEmptyStringSchema = Type.String({ minLength: 1 })
6
+
7
+ const webhookDispatchTargetSchema = Type.Object(
8
+ {
9
+ type: Type.Literal(`webhook`),
10
+ url: nonEmptyStringSchema,
11
+ subscription_id: Type.Optional(nonEmptyStringSchema),
12
+ },
13
+ { additionalProperties: false }
14
+ )
15
+
16
+ const runnerDispatchTargetSchema = Type.Object(
17
+ {
18
+ type: Type.Literal(`runner`),
19
+ runnerId: nonEmptyStringSchema,
20
+ subscription_id: Type.Optional(nonEmptyStringSchema),
21
+ },
22
+ { additionalProperties: false }
23
+ )
24
+
25
+ export const dispatchPolicySchema = Type.Object(
26
+ {
27
+ targets: Type.Tuple([
28
+ Type.Union([webhookDispatchTargetSchema, runnerDispatchTargetSchema]),
29
+ ]),
30
+ },
31
+ { additionalProperties: false }
32
+ )
33
+
34
+ export function parseDispatchPolicy(
35
+ value: unknown,
36
+ label = `dispatch_policy`
37
+ ): DispatchPolicy {
38
+ const validate = schemaValidator(dispatchPolicySchema)
39
+ if (validate(value)) return value as DispatchPolicy
40
+
41
+ const details = (validate.errors ?? [])
42
+ .map((error) => {
43
+ const path = error.instancePath || `/`
44
+ return `${path} ${error.message ?? `failed validation`}`
45
+ })
46
+ .join(`; `)
47
+ throw new Error(
48
+ details
49
+ ? `${label} does not match dispatch policy schema: ${details}`
50
+ : `${label} does not match dispatch policy schema`
51
+ )
52
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Interfaces for the Electric Agents adapter layer.
3
+ *
4
+ * Layer 2 adapters (one per SDK) implement CreateAdapter to translate
5
+ * SDK events into State Protocol writes via the provided writeEvent callback.
6
+ */
7
+
8
+ import type { AgentTool, StreamFn } from '@mariozechner/pi-agent-core'
9
+
10
+ /** A State Protocol event to be written via the adapter's writeEvent callback. */
11
+ export interface WriteEvent {
12
+ type: string
13
+ key: string
14
+ value: Record<string, unknown>
15
+ headers: {
16
+ operation: `insert` | `update`
17
+ [key: string]: unknown
18
+ }
19
+ }
20
+
21
+ /** Agent adapter — SDK-agnostic interface for the webhook handler. */
22
+ export interface AgentAdapter {
23
+ processMessage: (message: string) => Promise<void>
24
+ steer: (message: string) => void
25
+ isRunning: () => boolean
26
+ dispose: () => void
27
+ }
28
+
29
+ /** Configuration for an agent type (Layer 3 — pure data). */
30
+ export interface AgentTypeConfig {
31
+ systemPrompt: string
32
+ model: string
33
+ tools: Array<AgentTool>
34
+ }
35
+
36
+ /** Full agent type definition including registration metadata. */
37
+ export interface AgentTypeDefinition {
38
+ registration: {
39
+ name: string
40
+ description: string
41
+ }
42
+ systemPrompt: string
43
+ model: string
44
+ tools: Array<AgentTool>
45
+ }
46
+
47
+ /** Raw stream event as read from the entity's main stream. */
48
+ export interface StreamEvent {
49
+ specversion?: string
50
+ source?: string
51
+ id?: string
52
+ timestamp?: string
53
+ type?: string
54
+ key?: string
55
+ value?: Record<string, unknown>
56
+ headers?: {
57
+ operation?: `insert` | `update`
58
+ [key: string]: unknown
59
+ }
60
+ }
61
+
62
+ /** Factory function that each SDK adapter must implement. */
63
+ export type CreateAdapter = (opts: {
64
+ entityUrl: string
65
+ epoch: number
66
+ streamEvents: Array<StreamEvent>
67
+ writeEvent: (event: WriteEvent) => Promise<void>
68
+ config: AgentTypeConfig
69
+ streamFn?: StreamFn
70
+ }) => AgentAdapter
@@ -0,0 +1 @@
1
+ export { DEFAULT_OUTPUT_SCHEMAS } from '@electric-ax/agents-runtime'