@electric-ax/agents-server 0.4.15 → 0.4.17

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.
@@ -3,6 +3,7 @@ import {
3
3
  assertTags,
4
4
  buildTagsIndex,
5
5
  getEntitiesStreamPath,
6
+ hashString,
6
7
  normalizeTags,
7
8
  sourceRefForTags,
8
9
  } from '@electric-ax/agents-runtime'
@@ -13,7 +14,9 @@ import {
13
14
  } from '@electric-sql/client'
14
15
  import { serverLog } from './utils/log.js'
15
16
  import { electricUrlWithPath } from './utils/electric-url.js'
17
+ import { buildReadableEntitiesWhere } from './utils/server-utils.js'
16
18
  import { DEFAULT_TENANT_ID } from './tenant.js'
19
+ import { isBuiltInSystemPrincipalUrl } from './principal.js'
17
20
  import type { EntityBridgeRow, PostgresRegistry } from './entity-registry.js'
18
21
  import type { StreamClient } from './stream-client.js'
19
22
  import type {
@@ -30,7 +33,11 @@ import type {
30
33
  export interface EntityBridgeCoordinator {
31
34
  start(): Promise<void>
32
35
  stop(): Promise<void>
33
- register(tagsInput: unknown): Promise<{
36
+ register(
37
+ tagsInput: unknown,
38
+ principalUrl: string,
39
+ principalKind: string
40
+ ): Promise<{
34
41
  sourceRef: string
35
42
  streamUrl: string
36
43
  }>
@@ -115,19 +122,44 @@ function sqlStringLiteral(value: string): string {
115
122
 
116
123
  function buildTenantTagsWhereClause(
117
124
  tenantId: string,
118
- tags: EntityTags
125
+ tags: EntityTags,
126
+ principalUrl?: string,
127
+ principalKind?: string,
128
+ permissionBypass?: boolean
119
129
  ): string {
120
- return `tenant_id = ${sqlStringLiteral(tenantId)} AND (${buildTagsWhereClause(tags)})`
130
+ const readableWhere =
131
+ principalUrl && principalKind
132
+ ? buildReadableEntitiesWhere({
133
+ tenantId,
134
+ principalUrl,
135
+ principalKind,
136
+ permissionBypass,
137
+ })
138
+ : `tenant_id = ${sqlStringLiteral(tenantId)} AND FALSE`
139
+ return `${readableWhere} AND (${buildTagsWhereClause(tags)})`
121
140
  }
122
141
 
123
142
  function shapeEntityKey(message: ChangeMessage<EntityShapeRow>): string {
124
143
  return message.value.url
125
144
  }
126
145
 
146
+ function principalScopedSourceRef(
147
+ tagSourceRef: string,
148
+ principalUrl: string,
149
+ principalKind: string
150
+ ): string {
151
+ return `${tagSourceRef}-${hashString(
152
+ JSON.stringify({ principalKind, principalUrl })
153
+ )}`
154
+ }
155
+
127
156
  class EntityBridge {
128
157
  readonly sourceRef: string
129
158
  readonly tags: EntityTags
130
159
  readonly streamUrl: string
160
+ private readonly principalUrl?: string
161
+ private readonly principalKind?: string
162
+ private readonly permissionBypass: boolean
131
163
 
132
164
  private currentMembers = new Map<string, EntityMembershipRow>()
133
165
  private producer: IdempotentProducer | null = null
@@ -152,6 +184,9 @@ class EntityBridge {
152
184
  this.sourceRef = row.sourceRef
153
185
  this.tags = normalizeTags(row.tags)
154
186
  this.streamUrl = row.streamUrl
187
+ this.principalUrl = row.principalUrl
188
+ this.principalKind = row.principalKind
189
+ this.permissionBypass = isBuiltInSystemPrincipalUrl(row.principalUrl)
155
190
  this.initialShapeHandle = row.shapeHandle
156
191
  this.initialShapeOffset = row.shapeOffset
157
192
  }
@@ -316,7 +351,13 @@ class EntityBridge {
316
351
  url: electricUrlWithPath(this.electricUrl, `/v1/shape`).toString(),
317
352
  params: {
318
353
  table: `entities`,
319
- where: buildTenantTagsWhereClause(this.tenantId, this.tags),
354
+ where: buildTenantTagsWhereClause(
355
+ this.tenantId,
356
+ this.tags,
357
+ this.principalUrl,
358
+ this.principalKind,
359
+ this.permissionBypass
360
+ ),
320
361
  ...(this.electricSecret ? { secret: this.electricSecret } : {}),
321
362
  columns: [...ENTITY_SHAPE_COLUMNS],
322
363
  replica: `full`,
@@ -564,7 +605,11 @@ export class EntityBridgeManager implements EntityBridgeCoordinator {
564
605
  )
565
606
  }
566
607
 
567
- async register(tagsInput: unknown): Promise<{
608
+ async register(
609
+ tagsInput: unknown,
610
+ principalUrl: string,
611
+ principalKind: string
612
+ ): Promise<{
568
613
  sourceRef: string
569
614
  streamUrl: string
570
615
  }> {
@@ -573,13 +618,19 @@ export class EntityBridgeManager implements EntityBridgeCoordinator {
573
618
  }
574
619
 
575
620
  const tags = normalizeTags(assertTags(tagsInput))
576
- const sourceRef = sourceRefForTags(tags)
621
+ const sourceRef = principalScopedSourceRef(
622
+ sourceRefForTags(tags),
623
+ principalUrl,
624
+ principalKind
625
+ )
577
626
  const streamUrl = getEntitiesStreamPath(sourceRef)
578
627
 
579
628
  const row = await this.registry.upsertEntityBridge({
580
629
  sourceRef,
581
630
  tags,
582
631
  streamUrl,
632
+ principalUrl,
633
+ principalKind,
583
634
  })
584
635
  await this.registry.touchEntityBridge(sourceRef)
585
636
  await this.ensureBridge(row)
@@ -1,6 +1,7 @@
1
1
  import { createHash, randomUUID } from 'node:crypto'
2
2
  import fastq from 'fastq'
3
3
  import {
4
+ COMPOSER_INPUT_MESSAGE_TYPE,
4
5
  assertTags,
5
6
  entityStateSchema,
6
7
  getCronStreamPath,
@@ -11,6 +12,8 @@ import {
11
12
  manifestSharedStateKey,
12
13
  manifestSourceKey,
13
14
  resolveCronScheduleSpec,
15
+ validateComposerInputPayload,
16
+ validateSlashCommandDefinitions,
14
17
  } from '@electric-ax/agents-runtime'
15
18
  import type { EventPointer } from '@electric-ax/agents-runtime'
16
19
  import {
@@ -73,7 +76,6 @@ import type { EntityBridgeCoordinator } from './entity-bridge-manager.js'
73
76
  import type { Principal } from './principal.js'
74
77
 
75
78
  type SpawnPersistResult = [
76
- PromiseSettledResult<void>,
77
79
  PromiseSettledResult<void>,
78
80
  PromiseSettledResult<number>,
79
81
  ]
@@ -163,14 +165,14 @@ type ForkSubtreeOptions = {
163
165
  rootInstanceId?: string
164
166
  waitTimeoutMs?: number
165
167
  waitPollMs?: number
168
+ createdBy?: string
166
169
  /**
167
170
  * Optional anchor pointing at an event on the source root's `main` stream.
168
171
  * When set: only events at or before the pointer are kept on the root's
169
172
  * forked `main`, and the root's manifest is filtered so that descendants
170
173
  * spawned after the pointer are dropped from the fork (their now-orphan
171
174
  * subtrees are not forked). The pointer applies only to the root's
172
- * `main` stream — `error` and shared-state streams clone at HEAD
173
- * regardless.
175
+ * `main` stream; shared-state streams clone at HEAD regardless.
174
176
  */
175
177
  forkPointer?: EventPointer
176
178
  }
@@ -431,6 +433,7 @@ export class EntityManager {
431
433
  this.validateSchema(req.creation_schema)
432
434
  this.validateSchemaMap(req.inbox_schemas)
433
435
  this.validateSchemaMap(req.state_schemas)
436
+ this.validateSlashCommands(req.slash_commands)
434
437
  const defaultDispatchPolicy = req.default_dispatch_policy
435
438
  ? this.validateDispatchPolicy(req.default_dispatch_policy, {
436
439
  label: `default_dispatch_policy`,
@@ -445,6 +448,7 @@ export class EntityManager {
445
448
  creation_schema: req.creation_schema,
446
449
  inbox_schemas: req.inbox_schemas,
447
450
  state_schemas: req.state_schemas,
451
+ slash_commands: req.slash_commands,
448
452
  serve_endpoint: req.serve_endpoint,
449
453
  default_dispatch_policy: defaultDispatchPolicy,
450
454
  revision: existing ? existing.revision + 1 : 1,
@@ -497,7 +501,10 @@ export class EntityManager {
497
501
 
498
502
  async ensurePrincipal(principal: Principal): Promise<ElectricAgentsEntity> {
499
503
  const existing = await this.registry.getEntity(principal.url)
500
- if (existing) return existing
504
+ if (existing) {
505
+ await this.ensureUserPrincipal(principal)
506
+ return existing
507
+ }
501
508
  await this.ensurePrincipalEntityType()
502
509
  try {
503
510
  const entity = await this.spawn(`principal`, {
@@ -522,6 +529,7 @@ export class EntityManager {
522
529
  },
523
530
  })
524
531
  )
532
+ await this.ensureUserPrincipal(principal)
525
533
  return entity
526
534
  } catch (error) {
527
535
  if (
@@ -529,12 +537,21 @@ export class EntityManager {
529
537
  error.code === ErrCodeDuplicateURL
530
538
  ) {
531
539
  const raced = await this.registry.getEntity(principal.url)
532
- if (raced) return raced
540
+ if (raced) {
541
+ await this.ensureUserPrincipal(principal)
542
+ return raced
543
+ }
533
544
  }
534
545
  throw error
535
546
  }
536
547
  }
537
548
 
549
+ private async ensureUserPrincipal(principal: Principal): Promise<void> {
550
+ if (principal.kind === `user`) {
551
+ await this.registry.ensureUserForPrincipal(principal)
552
+ }
553
+ }
554
+
538
555
  // ==========================================================================
539
556
  // Spawn
540
557
  // ==========================================================================
@@ -624,7 +641,6 @@ export class EntityManager {
624
641
  ? principalUrl(instanceId)
625
642
  : `/${typeName}/${instanceId}`
626
643
  const mainPath = `${entityURL}/main`
627
- const errorPath = `${entityURL}/error`
628
644
 
629
645
  const subscriptionId = `${typeName}-handler`
630
646
 
@@ -676,7 +692,6 @@ export class EntityManager {
676
692
  url: entityURL,
677
693
  streams: {
678
694
  main: mainPath,
679
- error: errorPath,
680
695
  },
681
696
  subscription_id: subscriptionId,
682
697
  dispatch_policy: dispatchPolicy,
@@ -725,6 +740,19 @@ export class EntityManager {
725
740
  createdEvent as Record<string, unknown>,
726
741
  ]
727
742
 
743
+ const slashCommandTimestamp = new Date().toISOString()
744
+ for (const command of entityType.slash_commands ?? []) {
745
+ const slashCommandEvent = entityStateSchema.slashCommands.insert({
746
+ key: command.name,
747
+ value: {
748
+ ...command,
749
+ source: `static`,
750
+ updated_at: slashCommandTimestamp,
751
+ },
752
+ } as any)
753
+ initialEvents.push(slashCommandEvent as Record<string, unknown>)
754
+ }
755
+
728
756
  if (req.initialMessage !== undefined) {
729
757
  const msgNow = new Date().toISOString()
730
758
  const inboxEvent = entityStateSchema.inbox.insert({
@@ -732,6 +760,7 @@ export class EntityManager {
732
760
  value: {
733
761
  from: req.created_by ?? req.parent ?? `spawn`,
734
762
  payload: req.initialMessage,
763
+ message_type: req.initialMessageType,
735
764
  timestamp: msgNow,
736
765
  },
737
766
  } as any)
@@ -745,8 +774,8 @@ export class EntityManager {
745
774
  const queueEnterT0 = performance.now()
746
775
  const queueWaiting = this.spawnPersistQueue.length()
747
776
  const queueRunning = this.spawnPersistQueue.running()
748
- const [mainStreamResult, errorStreamResult, entityResult] =
749
- await this.spawnPersistQueue.push(async () => {
777
+ const [mainStreamResult, entityResult] = await this.spawnPersistQueue.push(
778
+ async () => {
750
779
  // Create entity first so it's visible in the DB before stream
751
780
  // creation can trigger webhooks that look up the entity.
752
781
  let entityTxid: number
@@ -756,41 +785,34 @@ export class EntityManager {
756
785
  )
757
786
  } catch (err) {
758
787
  return [
759
- { status: `fulfilled`, value: undefined },
760
788
  { status: `fulfilled`, value: undefined },
761
789
  { status: `rejected`, reason: err },
762
790
  ] as SpawnPersistResult
763
791
  }
764
792
 
765
- const [mainStreamResult, errorStreamResult] = await Promise.allSettled([
793
+ const [mainStreamResult] = await Promise.allSettled([
766
794
  this.streamClient.create(mainPath, {
767
795
  contentType,
768
796
  body: initialBody,
769
797
  }),
770
- this.streamClient.create(errorPath, { contentType }),
771
798
  ])
772
799
 
773
800
  return [
774
801
  mainStreamResult,
775
- errorStreamResult,
776
802
  { status: `fulfilled`, value: entityTxid },
777
803
  ] as SpawnPersistResult
778
- })
804
+ }
805
+ )
779
806
  const parallelMs = +(performance.now() - queueEnterT0).toFixed(2)
780
807
 
781
808
  if (
782
809
  mainStreamResult.status === `rejected` ||
783
- errorStreamResult.status === `rejected` ||
784
810
  entityResult.status === `rejected`
785
811
  ) {
786
812
  const entityReason =
787
813
  entityResult.status === `rejected` ? entityResult.reason : null
788
814
  const streamReason =
789
- mainStreamResult.status === `rejected`
790
- ? mainStreamResult.reason
791
- : errorStreamResult.status === `rejected`
792
- ? errorStreamResult.reason
793
- : null
815
+ mainStreamResult.status === `rejected` ? mainStreamResult.reason : null
794
816
  const isDuplicate = entityReason instanceof EntityAlreadyExistsError
795
817
  const isStreamConflict =
796
818
  !!streamReason &&
@@ -805,9 +827,6 @@ export class EntityManager {
805
827
  if (mainStreamResult.status === `fulfilled`) {
806
828
  rollbacks.push(this.streamClient.delete(mainPath))
807
829
  }
808
- if (errorStreamResult.status === `fulfilled`) {
809
- rollbacks.push(this.streamClient.delete(errorPath))
810
- }
811
830
  if (entityResult.status === `fulfilled`) {
812
831
  rollbacks.push(this.registry.deleteEntity(entityURL))
813
832
  }
@@ -834,9 +853,7 @@ export class EntityManager {
834
853
  const failure =
835
854
  mainStreamResult.status === `rejected`
836
855
  ? mainStreamResult.reason
837
- : errorStreamResult.status === `rejected`
838
- ? errorStreamResult.reason
839
- : (entityResult as PromiseRejectedResult).reason
856
+ : (entityResult as PromiseRejectedResult).reason
840
857
  if (failure instanceof Error) throw failure
841
858
  throw new ElectricAgentsError(
842
859
  `SPAWN_FAILED`,
@@ -1045,7 +1062,8 @@ export class EntityManager {
1045
1062
  const entityPlans = this.buildForkEntityPlans(
1046
1063
  effectiveSubtree,
1047
1064
  entityUrlMap,
1048
- stringMap
1065
+ stringMap,
1066
+ opts.createdBy
1049
1067
  )
1050
1068
 
1051
1069
  this.addForkLocks(
@@ -1077,13 +1095,6 @@ export class EntityManager {
1077
1095
  : undefined
1078
1096
  )
1079
1097
  createdStreams.push(plan.fork.streams.main)
1080
- // `error` always clones at HEAD — no canonical mapping
1081
- // between main-offset and error-offset.
1082
- await this.streamClient.fork(
1083
- plan.fork.streams.error,
1084
- plan.source.streams.error
1085
- )
1086
- createdStreams.push(plan.fork.streams.error)
1087
1098
  }
1088
1099
 
1089
1100
  for (const [sourceId, forkId] of sharedStateIdMap) {
@@ -1711,7 +1722,6 @@ export class EntityManager {
1711
1722
  for (const [sourceUrl, forkUrl] of entityUrlMap) {
1712
1723
  stringMap.set(sourceUrl, forkUrl)
1713
1724
  stringMap.set(`${sourceUrl}/main`, `${forkUrl}/main`)
1714
- stringMap.set(`${sourceUrl}/error`, `${forkUrl}/error`)
1715
1725
  }
1716
1726
  for (const [sourceId, forkId] of sharedStateIdMap) {
1717
1727
  stringMap.set(sourceId, forkId)
@@ -1726,7 +1736,8 @@ export class EntityManager {
1726
1736
  private buildForkEntityPlans(
1727
1737
  entitiesToFork: Array<ElectricAgentsEntity>,
1728
1738
  entityUrlMap: Map<string, string>,
1729
- stringMap: Map<string, string>
1739
+ stringMap: Map<string, string>,
1740
+ createdBy?: string
1730
1741
  ): Array<ForkEntityPlan> {
1731
1742
  const now = Date.now()
1732
1743
  return entitiesToFork.map((source) => {
@@ -1750,12 +1761,12 @@ export class EntityManager {
1750
1761
  status: `idle`,
1751
1762
  streams: {
1752
1763
  main: `${forkUrl}/main`,
1753
- error: `${forkUrl}/error`,
1754
1764
  },
1755
1765
  subscription_id: `${type}-handler`,
1756
1766
  write_token: randomUUID(),
1757
1767
  spawn_args: spawnArgs,
1758
1768
  parent,
1769
+ created_by: createdBy ?? source.created_by,
1759
1770
  created_at: now,
1760
1771
  updated_at: now,
1761
1772
  }
@@ -2047,12 +2058,7 @@ export class EntityManager {
2047
2058
  manifests: Map<string, Record<string, unknown>>
2048
2059
  ): Promise<void> {
2049
2060
  for (const [manifestKey, manifest] of manifests) {
2050
- await this.syncEntitiesManifestSource(
2051
- entityUrl,
2052
- manifestKey,
2053
- `upsert`,
2054
- manifest
2055
- )
2061
+ await this.syncManifestLinks(entityUrl, manifestKey, `upsert`, manifest)
2056
2062
 
2057
2063
  const wake = buildManifestWakeRegistration(
2058
2064
  entityUrl,
@@ -2125,6 +2131,7 @@ export class EntityManager {
2125
2131
  {
2126
2132
  entityUrl: targetUrl,
2127
2133
  from: senderUrl,
2134
+ from_agent: senderUrl,
2128
2135
  payload: manifest.payload,
2129
2136
  key: `scheduled-${producerId}`,
2130
2137
  type:
@@ -2191,7 +2198,7 @@ export class EntityManager {
2191
2198
  `msg-in-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
2192
2199
 
2193
2200
  const value: Record<string, unknown> = {
2194
- from: req.from,
2201
+ from: req.from_principal ?? req.from,
2195
2202
  payload: req.payload,
2196
2203
  timestamp: now,
2197
2204
  mode: req.mode ?? `immediate`,
@@ -2200,6 +2207,12 @@ export class EntityManager {
2200
2207
  ? `pending`
2201
2208
  : `processed`,
2202
2209
  }
2210
+ if (req.from_principal) {
2211
+ value.from_principal = req.from_principal
2212
+ }
2213
+ if (req.from_agent) {
2214
+ value.from_agent = req.from_agent
2215
+ }
2203
2216
  if (req.type) {
2204
2217
  value.message_type = req.type
2205
2218
  }
@@ -2610,14 +2623,21 @@ export class EntityManager {
2610
2623
  return updated
2611
2624
  }
2612
2625
 
2613
- async ensureEntitiesMembershipStream(tags: Record<string, string>): Promise<{
2626
+ async ensureEntitiesMembershipStream(
2627
+ tags: Record<string, string>,
2628
+ principal: { url: string; kind: string }
2629
+ ): Promise<{
2614
2630
  sourceRef: string
2615
2631
  streamUrl: string
2616
2632
  }> {
2617
2633
  if (!this.entityBridgeManager) {
2618
2634
  throw new Error(`Entity bridge manager not configured`)
2619
2635
  }
2620
- return this.entityBridgeManager.register(this.validateTags(tags))
2636
+ return this.entityBridgeManager.register(
2637
+ this.validateTags(tags),
2638
+ principal.url,
2639
+ principal.kind
2640
+ )
2621
2641
  }
2622
2642
 
2623
2643
  async writeManifestEntry(
@@ -2650,12 +2670,12 @@ export class EntityManager {
2650
2670
  await this.streamClient.appendIdempotent(entity.streams.main, encoded, {
2651
2671
  producerId: opts.producerId,
2652
2672
  })
2653
- await this.syncEntitiesManifestSource(entityUrl, key, operation, value)
2673
+ await this.syncManifestLinks(entityUrl, key, operation, value)
2654
2674
  return
2655
2675
  }
2656
2676
 
2657
2677
  await this.streamClient.append(entity.streams.main, encoded)
2658
- await this.syncEntitiesManifestSource(entityUrl, key, operation, value)
2678
+ await this.syncManifestLinks(entityUrl, key, operation, value)
2659
2679
  }
2660
2680
 
2661
2681
  async upsertCronSchedule(
@@ -2950,6 +2970,8 @@ export class EntityManager {
2950
2970
  {
2951
2971
  entityUrl,
2952
2972
  from: req.from,
2973
+ from_principal: req.from_principal,
2974
+ from_agent: req.from_agent,
2953
2975
  payload: req.payload,
2954
2976
  key: req.key,
2955
2977
  type: req.type,
@@ -3031,7 +3053,7 @@ export class EntityManager {
3031
3053
  })
3032
3054
  }
3033
3055
 
3034
- private async syncEntitiesManifestSource(
3056
+ private async syncManifestLinks(
3035
3057
  entityUrl: string,
3036
3058
  manifestKey: string,
3037
3059
  operation: `insert` | `update` | `upsert` | `delete`,
@@ -3044,6 +3066,14 @@ export class EntityManager {
3044
3066
  manifestKey,
3045
3067
  sourceRef
3046
3068
  )
3069
+
3070
+ const sharedStateId =
3071
+ operation === `delete` ? undefined : this.extractSharedStateId(value)
3072
+ await this.registry.replaceSharedStateLink(
3073
+ entityUrl,
3074
+ manifestKey,
3075
+ sharedStateId
3076
+ )
3047
3077
  }
3048
3078
 
3049
3079
  private extractEntitiesSourceRef(
@@ -3059,6 +3089,24 @@ export class EntityManager {
3059
3089
  return undefined
3060
3090
  }
3061
3091
 
3092
+ private extractSharedStateId(
3093
+ manifest?: Record<string, unknown>
3094
+ ): string | undefined {
3095
+ if (manifest?.kind === `shared-state` && typeof manifest.id === `string`) {
3096
+ return manifest.id
3097
+ }
3098
+
3099
+ if (manifest?.kind !== `source` || manifest.sourceType !== `db`) {
3100
+ return undefined
3101
+ }
3102
+
3103
+ if (typeof manifest.sourceRef === `string`) {
3104
+ return manifest.sourceRef
3105
+ }
3106
+ const config = isRecord(manifest.config) ? manifest.config : undefined
3107
+ return typeof config?.id === `string` ? config.id : undefined
3108
+ }
3109
+
3062
3110
  /**
3063
3111
  * Read a child entity's stream and extract concatenated text deltas
3064
3112
  * for a specific run, plus any error messages for that run.
@@ -3334,19 +3382,8 @@ export class EntityManager {
3334
3382
  return
3335
3383
  }
3336
3384
 
3337
- const errorCloseEvent = {
3338
- type: `signal`,
3339
- key: signalEvent.key,
3340
- value: signalEvent.value,
3341
- headers: signalEvent.headers,
3342
- }
3343
- const errorSignalData = this.encodeChangeEvent(
3344
- errorCloseEvent as unknown as Record<string, unknown>
3345
- )
3346
-
3347
3385
  for (const [streamPath, data] of [
3348
3386
  [entity.streams.main, signalData],
3349
- [entity.streams.error, errorSignalData],
3350
3387
  ] as const) {
3351
3388
  try {
3352
3389
  await this.streamClient.append(streamPath, data, { close: true })
@@ -3494,7 +3531,9 @@ export class EntityManager {
3494
3531
  creation_schema: existing.creation_schema,
3495
3532
  inbox_schemas: mergedInbox,
3496
3533
  state_schemas: mergedState,
3534
+ slash_commands: existing.slash_commands,
3497
3535
  serve_endpoint: existing.serve_endpoint,
3536
+ default_dispatch_policy: existing.default_dispatch_policy,
3498
3537
  revision: nextRevision,
3499
3538
  created_at: existing.created_at,
3500
3539
  updated_at: now,
@@ -3583,6 +3622,20 @@ export class EntityManager {
3583
3622
  }
3584
3623
  }
3585
3624
 
3625
+ private validateSlashCommands(input: unknown): void {
3626
+ const validationError = validateSlashCommandDefinitions(input)
3627
+ if (!validationError) {
3628
+ return
3629
+ }
3630
+
3631
+ throw new ElectricAgentsError(
3632
+ ErrCodeSchemaValidationFailed,
3633
+ validationError.message,
3634
+ 422,
3635
+ validationError.details
3636
+ )
3637
+ }
3638
+
3586
3639
  private async validateSendRequest(
3587
3640
  entityUrl: string,
3588
3641
  req: SendRequest
@@ -3599,7 +3652,17 @@ export class EntityManager {
3599
3652
  )
3600
3653
  }
3601
3654
 
3602
- if (req.type && entity.type) {
3655
+ if (req.type === COMPOSER_INPUT_MESSAGE_TYPE) {
3656
+ const valErr = validateComposerInputPayload(req.payload)
3657
+ if (valErr) {
3658
+ throw new ElectricAgentsError(
3659
+ ErrCodeSchemaValidationFailed,
3660
+ valErr.message,
3661
+ 422,
3662
+ valErr.details
3663
+ )
3664
+ }
3665
+ } else if (req.type && entity.type) {
3603
3666
  const { inboxSchemas } = await this.getEffectiveSchemas(entity)
3604
3667
  if (inboxSchemas) {
3605
3668
  const schema = inboxSchemas[req.type]