@electric-ax/agents-server 0.4.16 → 0.4.18

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/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import postgres from "postgres";
8
8
  import { and, desc, eq, inArray, lt, ne, sql } from "drizzle-orm";
9
9
  import { bigint, bigserial, boolean, check, index, integer, jsonb, pgTable, primaryKey, serial, text, timestamp, unique } from "drizzle-orm/pg-core";
10
10
  import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, randomUUID, sign } from "node:crypto";
11
- import { appendPathToUrl, assertTags, buildEventSourceManifestEntry, buildTagsIndex, entityStateSchema, eventSourceSubscriptionManifestKey, getCronStreamPath, getCronStreamPathFromSpec, getEntitiesStreamPath, getNextCronFireAt, getSharedStateStreamPath, getWebhookStreamPath, hashString, manifestChildKey, manifestSharedStateKey, manifestSourceKey, normalizeTags, parseCronStreamPath, resolveCronScheduleSpec, resolveEventSourceSubscription, sourceRefForTags, verifyWebhookSignature } from "@electric-ax/agents-runtime";
11
+ import { COMPOSER_INPUT_MESSAGE_TYPE, appendPathToUrl, assertTags, buildEventSourceManifestEntry, buildTagsIndex, entityStateSchema, eventSourceSubscriptionManifestKey, getCronStreamPath, getCronStreamPathFromSpec, getEntitiesStreamPath, getNextCronFireAt, getSharedStateStreamPath, getWebhookStreamPath, hashString, manifestChildKey, manifestSharedStateKey, manifestSourceKey, normalizeTags, parseCronStreamPath, resolveCronScheduleSpec, resolveEventSourceSubscription, sourceRefForTags, validateComposerInputPayload, validateSlashCommandDefinitions, verifyWebhookSignature } from "@electric-ax/agents-runtime";
12
12
  import { DurableStream, DurableStreamError, FetchError, IdempotentProducer } from "@durable-streams/client";
13
13
  import { ShapeStream, isChangeMessage, isControlMessage } from "@electric-sql/client";
14
14
  import pino from "pino";
@@ -49,6 +49,7 @@ const entityTypes = pgTable(`entity_types`, {
49
49
  creationSchema: jsonb(`creation_schema`),
50
50
  inboxSchemas: jsonb(`inbox_schemas`),
51
51
  stateSchemas: jsonb(`state_schemas`),
52
+ slashCommands: jsonb(`slash_commands`),
52
53
  serveEndpoint: text(`serve_endpoint`),
53
54
  defaultDispatchPolicy: jsonb(`default_dispatch_policy`),
54
55
  revision: integer(`revision`).notNull().default(1),
@@ -820,6 +821,7 @@ var PostgresRegistry = class {
820
821
  creationSchema: et.creation_schema ?? null,
821
822
  inboxSchemas: et.inbox_schemas ?? null,
822
823
  stateSchemas: et.state_schemas ?? null,
824
+ slashCommands: et.slash_commands ?? null,
823
825
  serveEndpoint: et.serve_endpoint ?? null,
824
826
  defaultDispatchPolicy: et.default_dispatch_policy ?? null,
825
827
  revision: et.revision,
@@ -832,6 +834,7 @@ var PostgresRegistry = class {
832
834
  creationSchema: et.creation_schema ?? null,
833
835
  inboxSchemas: et.inbox_schemas ?? null,
834
836
  stateSchemas: et.state_schemas ?? null,
837
+ slashCommands: et.slash_commands ?? null,
835
838
  serveEndpoint: et.serve_endpoint ?? null,
836
839
  defaultDispatchPolicy: et.default_dispatch_policy ?? null,
837
840
  revision: et.revision,
@@ -849,6 +852,7 @@ var PostgresRegistry = class {
849
852
  creationSchema: et.creation_schema ?? null,
850
853
  inboxSchemas: et.inbox_schemas ?? null,
851
854
  stateSchemas: et.state_schemas ?? null,
855
+ slashCommands: et.slash_commands ?? null,
852
856
  serveEndpoint: et.serve_endpoint ?? null,
853
857
  defaultDispatchPolicy: et.default_dispatch_policy ?? null,
854
858
  revision: et.revision,
@@ -875,6 +879,7 @@ var PostgresRegistry = class {
875
879
  creationSchema: et.creation_schema ?? null,
876
880
  inboxSchemas: et.inbox_schemas ?? null,
877
881
  stateSchemas: et.state_schemas ?? null,
882
+ slashCommands: et.slash_commands ?? null,
878
883
  serveEndpoint: et.serve_endpoint ?? null,
879
884
  defaultDispatchPolicy: et.default_dispatch_policy ?? null,
880
885
  revision: et.revision,
@@ -1465,6 +1470,7 @@ var PostgresRegistry = class {
1465
1470
  creation_schema: row.creationSchema,
1466
1471
  inbox_schemas: row.inboxSchemas,
1467
1472
  state_schemas: row.stateSchemas,
1473
+ slash_commands: row.slashCommands ?? void 0,
1468
1474
  serve_endpoint: row.serveEndpoint ?? void 0,
1469
1475
  default_dispatch_policy: row.defaultDispatchPolicy ?? void 0,
1470
1476
  revision: row.revision,
@@ -3358,6 +3364,7 @@ var EntityManager = class {
3358
3364
  this.validateSchema(req.creation_schema);
3359
3365
  this.validateSchemaMap(req.inbox_schemas);
3360
3366
  this.validateSchemaMap(req.state_schemas);
3367
+ this.validateSlashCommands(req.slash_commands);
3361
3368
  const defaultDispatchPolicy = req.default_dispatch_policy ? this.validateDispatchPolicy(req.default_dispatch_policy, { label: `default_dispatch_policy` }) : void 0;
3362
3369
  const existing = await this.registry.getEntityType(req.name);
3363
3370
  const now = new Date().toISOString();
@@ -3367,6 +3374,7 @@ var EntityManager = class {
3367
3374
  creation_schema: req.creation_schema,
3368
3375
  inbox_schemas: req.inbox_schemas,
3369
3376
  state_schemas: req.state_schemas,
3377
+ slash_commands: req.slash_commands,
3370
3378
  serve_endpoint: req.serve_endpoint,
3371
3379
  default_dispatch_policy: defaultDispatchPolicy,
3372
3380
  revision: existing ? existing.revision + 1 : 1,
@@ -3528,6 +3536,18 @@ var EntityManager = class {
3528
3536
  }
3529
3537
  });
3530
3538
  const initialEvents = [createdEvent];
3539
+ const slashCommandTimestamp = new Date().toISOString();
3540
+ for (const command of entityType.slash_commands ?? []) {
3541
+ const slashCommandEvent = entityStateSchema.slashCommands.insert({
3542
+ key: command.name,
3543
+ value: {
3544
+ ...command,
3545
+ source: `static`,
3546
+ updated_at: slashCommandTimestamp
3547
+ }
3548
+ });
3549
+ initialEvents.push(slashCommandEvent);
3550
+ }
3531
3551
  if (req.initialMessage !== void 0) {
3532
3552
  const msgNow = new Date().toISOString();
3533
3553
  const inboxEvent = entityStateSchema.inbox.insert({
@@ -3535,6 +3555,7 @@ var EntityManager = class {
3535
3555
  value: {
3536
3556
  from: req.created_by ?? req.parent ?? `spawn`,
3537
3557
  payload: req.initialMessage,
3558
+ message_type: req.initialMessageType,
3538
3559
  timestamp: msgNow
3539
3560
  }
3540
3561
  });
@@ -3614,9 +3635,10 @@ var EntityManager = class {
3614
3635
  const workLocks = new Set();
3615
3636
  const writeEntityLocks = new Set();
3616
3637
  const writeStreamLocks = new Set();
3638
+ const usePointerPath = opts.forkPointer !== void 0 || opts.anchor !== void 0;
3617
3639
  try {
3618
3640
  let sourceTree;
3619
- if (opts.forkPointer) {
3641
+ if (usePointerPath) {
3620
3642
  const rootEntity = await this.registry.getEntity(rootUrl);
3621
3643
  if (!rootEntity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
3622
3644
  if (isTerminalEntityStatus(rootEntity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Cannot fork terminal entity "${rootEntity.url}"`, 409);
@@ -3625,10 +3647,13 @@ var EntityManager = class {
3625
3647
  const sourceRoot = sourceTree[0];
3626
3648
  if (sourceRoot.parent) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Only top-level entities can be forked`, 400);
3627
3649
  let preFilteredRoot;
3628
- if (opts.forkPointer) {
3650
+ let effectiveForkPointer = opts.forkPointer;
3651
+ if (usePointerPath) {
3629
3652
  const sourceEvents = await this.streamClient.readJson(sourceRoot.streams.main);
3630
3653
  const flat = sourceEvents.flatMap((item) => Array.isArray(item) ? item : [item]);
3631
- const target = this.resolveForkPointerTarget(flat, opts.forkPointer, sourceRoot.streams.main);
3654
+ if (!effectiveForkPointer && opts.anchor === `latest_completed_run`) effectiveForkPointer = this.resolveLatestCompletedRunPointer(flat, sourceRoot.streams.main);
3655
+ if (!effectiveForkPointer) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Internal: pointer-path fork with no resolved pointer`, 500);
3656
+ const target = this.resolveForkPointerTarget(flat, effectiveForkPointer, sourceRoot.streams.main);
3632
3657
  const filteredEvents = flat.slice(0, target);
3633
3658
  const rootManifests = this.reduceStateRows(filteredEvents, `manifest`);
3634
3659
  const sharedStateIds = new Set();
@@ -3641,7 +3666,7 @@ var EntityManager = class {
3641
3666
  };
3642
3667
  }
3643
3668
  const effectiveSubtree = preFilteredRoot ? this.computeEffectiveSubtree(sourceTree, sourceRoot.url, preFilteredRoot.manifests) : sourceTree;
3644
- if (opts.forkPointer) {
3669
+ if (usePointerPath) {
3645
3670
  const descendants = effectiveSubtree.filter((entity) => entity.url !== sourceRoot.url);
3646
3671
  if (descendants.length > 0) await this.waitForGivenEntitiesIdle(descendants, opts, workLocks);
3647
3672
  }
@@ -3666,6 +3691,14 @@ var EntityManager = class {
3666
3691
  const sharedStateIdMap = await this.buildForkSharedStateIdMap(snapshot.sharedStateIds, suffix);
3667
3692
  const stringMap = this.buildForkStringMap(entityUrlMap, sharedStateIdMap);
3668
3693
  const entityPlans = this.buildForkEntityPlans(effectiveSubtree, entityUrlMap, stringMap, opts.createdBy);
3694
+ const rootPlanForOverrides = entityPlans.find((plan) => plan.source.url === rootUrl);
3695
+ if (rootPlanForOverrides) {
3696
+ if (opts.parent !== void 0) rootPlanForOverrides.fork.parent = opts.parent;
3697
+ if (opts.tags !== void 0) rootPlanForOverrides.fork.tags = {
3698
+ ...rootPlanForOverrides.fork.tags ?? {},
3699
+ ...opts.tags
3700
+ };
3701
+ }
3669
3702
  this.addForkLocks(this.forkWriteLockedEntities, effectiveSubtree.map((entity) => entity.url), writeEntityLocks);
3670
3703
  this.addForkLocks(this.forkWriteLockedStreams, [...snapshot.sharedStateIds].map((id) => getSharedStateStreamPath(id)), writeStreamLocks);
3671
3704
  const createdStreams = [];
@@ -3674,7 +3707,7 @@ var EntityManager = class {
3674
3707
  try {
3675
3708
  for (const plan of entityPlans) {
3676
3709
  const isRoot = plan.source.url === rootUrl;
3677
- await this.streamClient.fork(plan.fork.streams.main, plan.source.streams.main, isRoot && opts.forkPointer ? { forkPointer: opts.forkPointer } : void 0);
3710
+ await this.streamClient.fork(plan.fork.streams.main, plan.source.streams.main, isRoot && effectiveForkPointer ? { forkPointer: effectiveForkPointer } : void 0);
3678
3711
  createdStreams.push(plan.fork.streams.main);
3679
3712
  }
3680
3713
  for (const [sourceId, forkId] of sharedStateIdMap) {
@@ -3701,6 +3734,20 @@ var EntityManager = class {
3701
3734
  await this.registry.createEntity(plan.fork);
3702
3735
  createdEntities.push(plan.fork.url);
3703
3736
  }
3737
+ if (opts.wake !== void 0) {
3738
+ const rootPlan = entityPlans.find((plan) => plan.source.url === rootUrl);
3739
+ if (rootPlan) await this.wakeRegistry.register({
3740
+ tenantId: this.tenantId,
3741
+ subscriberUrl: opts.wake.subscriberUrl,
3742
+ sourceUrl: rootPlan.fork.url,
3743
+ condition: opts.wake.condition,
3744
+ debounceMs: opts.wake.debounceMs,
3745
+ timeoutMs: opts.wake.timeoutMs,
3746
+ oneShot: false,
3747
+ includeResponse: opts.wake.includeResponse,
3748
+ manifestKey: opts.wake.manifestKey
3749
+ });
3750
+ }
3704
3751
  for (const plan of entityPlans) {
3705
3752
  const manifests = activeManifestsByEntity.get(plan.fork.url) ?? new Map();
3706
3753
  await this.materializeForkManifestSideEffects(plan.fork.url, manifests);
@@ -3861,6 +3908,45 @@ var EntityManager = class {
3861
3908
  return positionAtAnchor + pointer.subOffset;
3862
3909
  }
3863
3910
  /**
3911
+ * Find an `EventPointer` that addresses the most recent `runs` row on
3912
+ * the source's `main` stream with `status === 'completed'`. Mirrors the
3913
+ * eligibility rule the per-row "Fork from here" UI applies — only
3914
+ * completed runs are valid fork anchors; `started`/`failed` rows are
3915
+ * skipped.
3916
+ *
3917
+ * The pointer is computed in the same coordinate system the runtime
3918
+ * mints pointers in (see entity-stream-db's onBeforeBatch): for a row
3919
+ * R in log entry E, the pointer is `{ offset: P, subOffset: K }` where
3920
+ * `P` is the END offset of the log entry preceding E (or `null` for
3921
+ * stream start) and `K` is R's 1-indexed position within E.
3922
+ */
3923
+ resolveLatestCompletedRunPointer(events, streamPath) {
3924
+ let priorEntryOffset = null;
3925
+ let currentEntryOffset = null;
3926
+ let positionInEntry = 0;
3927
+ let latest = null;
3928
+ for (const event of events) {
3929
+ const headers = isRecord(event.headers) ? event.headers : void 0;
3930
+ const eventOffset = typeof headers?.offset === `string` ? headers.offset : null;
3931
+ if (eventOffset !== currentEntryOffset) {
3932
+ priorEntryOffset = currentEntryOffset;
3933
+ currentEntryOffset = eventOffset;
3934
+ positionInEntry = 0;
3935
+ }
3936
+ positionInEntry++;
3937
+ if (event.type !== `run`) continue;
3938
+ if (headers?.operation === `delete`) continue;
3939
+ if (!isRecord(event.value)) continue;
3940
+ if (event.value.status !== `completed`) continue;
3941
+ latest = {
3942
+ offset: priorEntryOffset,
3943
+ subOffset: positionInEntry
3944
+ };
3945
+ }
3946
+ if (!latest) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Source ${streamPath} has no completed run to fork from`, 400);
3947
+ return latest;
3948
+ }
3949
+ /**
3864
3950
  * Compute the subset of `sourceTree` that survives the manifest filter
3865
3951
  * applied at the root. After filtering the root's manifest at the fork
3866
3952
  * pointer, only children whose manifest entries landed at or before the
@@ -4980,7 +5066,9 @@ var EntityManager = class {
4980
5066
  creation_schema: existing.creation_schema,
4981
5067
  inbox_schemas: mergedInbox,
4982
5068
  state_schemas: mergedState,
5069
+ slash_commands: existing.slash_commands,
4983
5070
  serve_endpoint: existing.serve_endpoint,
5071
+ default_dispatch_policy: existing.default_dispatch_policy,
4984
5072
  revision: nextRevision,
4985
5073
  created_at: existing.created_at,
4986
5074
  updated_at: now
@@ -5034,11 +5122,19 @@ var EntityManager = class {
5034
5122
  throw new ElectricAgentsError(ErrCodeInvalidRequest, error instanceof Error ? error.message : `Invalid tags`, 400);
5035
5123
  }
5036
5124
  }
5125
+ validateSlashCommands(input) {
5126
+ const validationError = validateSlashCommandDefinitions(input);
5127
+ if (!validationError) return;
5128
+ throw new ElectricAgentsError(ErrCodeSchemaValidationFailed, validationError.message, 422, validationError.details);
5129
+ }
5037
5130
  async validateSendRequest(entityUrl, req) {
5038
5131
  const entity = await this.registry.getEntity(entityUrl);
5039
5132
  if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
5040
5133
  if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
5041
- if (req.type && entity.type) {
5134
+ if (req.type === COMPOSER_INPUT_MESSAGE_TYPE) {
5135
+ const valErr = validateComposerInputPayload(req.payload);
5136
+ if (valErr) throw new ElectricAgentsError(ErrCodeSchemaValidationFailed, valErr.message, 422, valErr.details);
5137
+ } else if (req.type && entity.type) {
5042
5138
  const { inboxSchemas } = await this.getEffectiveSchemas(entity);
5043
5139
  if (inboxSchemas) {
5044
5140
  const schema = inboxSchemas[req.type];
@@ -7111,7 +7207,7 @@ function buildElectricProxyTarget(options) {
7111
7207
  permissionBypass: options.permissionBypass
7112
7208
  }));
7113
7209
  } else if (table === `entity_types`) {
7114
- target.searchParams.set(`columns`, `"tenant_id","name","description","creation_schema","inbox_schemas","state_schemas","serve_endpoint","default_dispatch_policy","revision","created_at","updated_at"`);
7210
+ target.searchParams.set(`columns`, `"tenant_id","name","description","creation_schema","inbox_schemas","state_schemas","slash_commands","serve_endpoint","default_dispatch_policy","revision","created_at","updated_at"`);
7115
7211
  applyShapeWhere(target, buildSpawnableEntityTypesWhere({
7116
7212
  tenantId: options.tenantId,
7117
7213
  principalUrl: options.principalUrl ?? ``,
@@ -7919,6 +8015,7 @@ const spawnBodySchema = Type.Object({
7919
8015
  sandbox: Type.Optional(sandboxChoiceSchema),
7920
8016
  initialMessage: Type.Optional(Type.Unknown()),
7921
8017
  grants: Type.Optional(Type.Array(entityPermissionGrantInputSchema)),
8018
+ initialMessageType: Type.Optional(Type.String()),
7922
8019
  wake: Type.Optional(Type.Object({
7923
8020
  subscriberUrl: Type.String(),
7924
8021
  condition: wakeConditionSchema,
@@ -7956,6 +8053,14 @@ function agentUrlPath(value) {
7956
8053
  return value;
7957
8054
  }
7958
8055
  }
8056
+ async function hasValidAgentWriteToken(request, ctx, fromAgent) {
8057
+ const agentUrl = agentUrlPath(fromAgent);
8058
+ const token = writeTokenFromRequest(request);
8059
+ if (!token) return false;
8060
+ const agentEntity = await ctx.entityManager.registry.getEntity(agentUrl);
8061
+ if (!agentEntity) return false;
8062
+ return ctx.entityManager.isValidWriteToken(agentEntity, token);
8063
+ }
7959
8064
  const inboxMessageBodySchema = Type.Object({
7960
8065
  payload: Type.Optional(Type.Unknown()),
7961
8066
  position: Type.Optional(Type.String()),
@@ -7977,7 +8082,19 @@ const forkBodySchema = Type.Object({
7977
8082
  fork_pointer: Type.Optional(Type.Object({
7978
8083
  offset: Type.Union([Type.String(), Type.Null()]),
7979
8084
  sub_offset: Type.Number()
7980
- }))
8085
+ })),
8086
+ anchor: Type.Optional(Type.Literal(`latest_completed_run`)),
8087
+ parent: Type.Optional(Type.String()),
8088
+ wake: Type.Optional(Type.Object({
8089
+ subscriberUrl: Type.String(),
8090
+ condition: wakeConditionSchema,
8091
+ debounceMs: Type.Optional(Type.Number()),
8092
+ timeoutMs: Type.Optional(Type.Number()),
8093
+ includeResponse: Type.Optional(Type.Boolean()),
8094
+ manifestKey: Type.Optional(Type.String())
8095
+ })),
8096
+ initialMessage: Type.Optional(Type.Unknown()),
8097
+ tags: Type.Optional(stringRecordSchema$1)
7981
8098
  });
7982
8099
  const setTagBodySchema = Type.Object({ value: Type.String() });
7983
8100
  const entitySignalSchema = Type.Union([
@@ -8364,8 +8481,18 @@ async function forkEntity(request, ctx) {
8364
8481
  const principalMutationError = rejectPrincipalEntityMutation(request, `forked`);
8365
8482
  if (principalMutationError) return principalMutationError;
8366
8483
  const parsed = routeBody(request);
8484
+ if (parsed.fork_pointer && parsed.anchor) return apiError(400, ErrCodeInvalidRequest, `fork_pointer and anchor are mutually exclusive`);
8367
8485
  const { entityUrl, entity } = requireExistingEntityRoute(request);
8368
8486
  await assertDispatchPolicyAllowed(ctx, entity.dispatch_policy);
8487
+ if (parsed.parent !== void 0) {
8488
+ const parent = await ctx.entityManager.registry.getEntity(parsed.parent);
8489
+ if (!parent) return apiError(404, ErrCodeNotFound, `Parent entity not found`);
8490
+ if (!await canAccessEntity(ctx, parent, `spawn`, request)) return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to spawn children from ${parent.url}`);
8491
+ }
8492
+ if (parsed.wake !== void 0) {
8493
+ if (parsed.parent === void 0) return apiError(400, ErrCodeInvalidRequest, `wake requires parent (the fork's wake fires to its parent)`);
8494
+ if (parsed.wake.subscriberUrl !== parsed.parent) return apiError(401, ErrCodeUnauthorized, `wake.subscriberUrl must match parent`);
8495
+ }
8369
8496
  const result = await ctx.entityManager.forkSubtree(entityUrl, {
8370
8497
  rootInstanceId: parsed.instance_id,
8371
8498
  waitTimeoutMs: parsed.waitTimeoutMs,
@@ -8373,9 +8500,17 @@ async function forkEntity(request, ctx) {
8373
8500
  ...parsed.fork_pointer && { forkPointer: {
8374
8501
  offset: parsed.fork_pointer.offset,
8375
8502
  subOffset: parsed.fork_pointer.sub_offset
8376
- } }
8503
+ } },
8504
+ ...parsed.anchor && { anchor: parsed.anchor },
8505
+ ...parsed.parent !== void 0 && { parent: parsed.parent },
8506
+ ...parsed.wake !== void 0 && { wake: parsed.wake },
8507
+ ...parsed.tags !== void 0 && { tags: parsed.tags }
8377
8508
  });
8378
8509
  for (const forkedEntity of result.entities) await linkEntityDispatchSubscription(ctx, forkedEntity);
8510
+ if (parsed.initialMessage !== void 0) await ctx.entityManager.send(result.root.url, {
8511
+ from: parsed.parent ?? ctx.principal.url,
8512
+ payload: parsed.initialMessage
8513
+ });
8379
8514
  return json({
8380
8515
  root: toPublicEntity(result.root),
8381
8516
  entities: result.entities.map((entity$1) => toPublicEntity(entity$1))
@@ -8388,7 +8523,10 @@ async function sendEntity(request, ctx) {
8388
8523
  if (parsed.from_principal !== void 0 && parsed.from_principal !== principal.url) return apiError(400, ErrCodeInvalidRequest, `Request from_principal must match Electric-Principal`);
8389
8524
  if (parsed.from_agent !== void 0) {
8390
8525
  const principalAgentUrl = agentUrlForPrincipal(principal);
8391
- if (agentUrlPath(parsed.from_agent) !== principalAgentUrl) return apiError(400, ErrCodeInvalidRequest, `Request from_agent must match authenticated agent principal`);
8526
+ const fromAgentUrl = agentUrlPath(parsed.from_agent);
8527
+ const matchesPrincipalAgent = fromAgentUrl === principalAgentUrl;
8528
+ const hasAgentWriteToken = await hasValidAgentWriteToken(request, ctx, parsed.from_agent);
8529
+ if (!matchesPrincipalAgent && !hasAgentWriteToken) return apiError(400, ErrCodeInvalidRequest, `Request from_agent must match authenticated agent principal`);
8392
8530
  }
8393
8531
  await ctx.entityManager.ensurePrincipal(principal);
8394
8532
  const { entityUrl, entity } = requireExistingEntityRoute(request);
@@ -8474,6 +8612,7 @@ async function spawnEntity(request, ctx) {
8474
8612
  dispatch_policy: dispatchPolicy,
8475
8613
  sandbox: parsed.sandbox,
8476
8614
  initialMessage: void 0,
8615
+ initialMessageType: void 0,
8477
8616
  wake: parsed.wake,
8478
8617
  created_by: principal.url
8479
8618
  });
@@ -8492,7 +8631,8 @@ async function spawnEntity(request, ctx) {
8492
8631
  if (linkBeforeInitialMessage) await linkEntityDispatchSubscription(ctx, entity);
8493
8632
  if (parsed.initialMessage !== void 0) await ctx.entityManager.send(entity.url, {
8494
8633
  from: principal.url,
8495
- payload: parsed.initialMessage
8634
+ payload: parsed.initialMessage,
8635
+ type: parsed.initialMessageType
8496
8636
  });
8497
8637
  if (!linkBeforeInitialMessage) await linkEntityDispatchSubscription(ctx, entity);
8498
8638
  return json({
@@ -8539,6 +8679,21 @@ async function signalEntity(request, ctx) {
8539
8679
  //#region src/routing/entity-types-router.ts
8540
8680
  const jsonObjectSchema = Type.Record(Type.String(), Type.Unknown());
8541
8681
  const schemaMapSchema = Type.Record(Type.String(), jsonObjectSchema);
8682
+ const slashCommandArgumentSchema = Type.Object({
8683
+ name: Type.String(),
8684
+ type: Type.Union([
8685
+ Type.Literal(`string`),
8686
+ Type.Literal(`number`),
8687
+ Type.Literal(`boolean`)
8688
+ ]),
8689
+ required: Type.Optional(Type.Boolean()),
8690
+ description: Type.Optional(Type.String())
8691
+ }, { additionalProperties: false });
8692
+ const slashCommandSchema = Type.Object({
8693
+ name: Type.String(),
8694
+ description: Type.Optional(Type.String()),
8695
+ arguments: Type.Optional(Type.Array(slashCommandArgumentSchema))
8696
+ }, { additionalProperties: false });
8542
8697
  const typePermissionGrantInputSchema = Type.Object({
8543
8698
  subject_kind: Type.Union([Type.Literal(`principal`), Type.Literal(`principal_kind`)]),
8544
8699
  subject_value: Type.String(),
@@ -8551,6 +8706,7 @@ const registerEntityTypeBodySchema = Type.Object({
8551
8706
  creation_schema: Type.Optional(jsonObjectSchema),
8552
8707
  inbox_schemas: Type.Optional(schemaMapSchema),
8553
8708
  state_schemas: Type.Optional(schemaMapSchema),
8709
+ slash_commands: Type.Optional(Type.Array(slashCommandSchema)),
8554
8710
  serve_endpoint: Type.Optional(Type.String()),
8555
8711
  default_dispatch_policy: Type.Optional(dispatchPolicySchema),
8556
8712
  permission_grants: Type.Optional(Type.Array(typePermissionGrantInputSchema))
@@ -8692,6 +8848,7 @@ function normalizeEntityTypeRequest(parsed) {
8692
8848
  creation_schema: parsed.creation_schema,
8693
8849
  inbox_schemas: parsed.inbox_schemas,
8694
8850
  state_schemas: parsed.state_schemas,
8851
+ slash_commands: parsed.slash_commands,
8695
8852
  serve_endpoint: serveEndpoint,
8696
8853
  default_dispatch_policy: parsed.default_dispatch_policy ?? (serveEndpoint ? { targets: [{
8697
8854
  type: `webhook`,
@@ -0,0 +1 @@
1
+ ALTER TABLE "entity_types" ADD COLUMN "slash_commands" jsonb;
@@ -99,6 +99,13 @@
99
99
  "when": 1780588695000,
100
100
  "tag": "0013_worker_user_manage_permission",
101
101
  "breakpoints": true
102
+ },
103
+ {
104
+ "idx": 14,
105
+ "version": "7",
106
+ "when": 1780600000000,
107
+ "tag": "0014_entity_type_slash_commands",
108
+ "breakpoints": true
102
109
  }
103
110
  ]
104
111
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electric-ax/agents-server",
3
- "version": "0.4.16",
3
+ "version": "0.4.18",
4
4
  "description": "Electric Agents entity runtime server",
5
5
  "author": "Durable Stream contributors",
6
6
  "bin": {
@@ -54,7 +54,7 @@
54
54
  "pino-pretty": "^13.0.0",
55
55
  "postgres": "^3.4.0",
56
56
  "undici": "^7.24.7",
57
- "@electric-ax/agents-runtime": "0.3.9"
57
+ "@electric-ax/agents-runtime": "0.3.11"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@types/node": "^22.19.15",
@@ -65,9 +65,9 @@
65
65
  "tsx": "^4.19.0",
66
66
  "typescript": "^5.0.0",
67
67
  "vitest": "^4.1.0",
68
- "@electric-ax/agents": "0.4.13",
69
- "@electric-ax/agents-server-conformance-tests": "0.1.11",
70
- "@electric-ax/agents-server-ui": "0.4.16"
68
+ "@electric-ax/agents-server-ui": "0.4.18",
69
+ "@electric-ax/agents": "0.4.15",
70
+ "@electric-ax/agents-server-conformance-tests": "0.1.11"
71
71
  },
72
72
  "files": [
73
73
  "dist",
package/src/db/schema.ts CHANGED
@@ -24,6 +24,7 @@ export const entityTypes = pgTable(
24
24
  creationSchema: jsonb(`creation_schema`),
25
25
  inboxSchemas: jsonb(`inbox_schemas`),
26
26
  stateSchemas: jsonb(`state_schemas`),
27
+ slashCommands: jsonb(`slash_commands`),
27
28
  serveEndpoint: text(`serve_endpoint`),
28
29
  defaultDispatchPolicy: jsonb(`default_dispatch_policy`),
29
30
  revision: integer(`revision`).notNull().default(1),
@@ -4,6 +4,7 @@
4
4
 
5
5
  import type {
6
6
  PullWakeRunnerHealth,
7
+ SlashCommandDefinition,
7
8
  WebhookNotification,
8
9
  } from '@electric-ax/agents-runtime'
9
10
  import type { Principal } from './principal.js'
@@ -499,6 +500,7 @@ export interface ElectricAgentsEntityType {
499
500
  creation_schema?: Record<string, unknown>
500
501
  inbox_schemas?: Record<string, Record<string, unknown>>
501
502
  state_schemas?: Record<string, Record<string, unknown>>
503
+ slash_commands?: Array<SlashCommandDefinition>
502
504
  serve_endpoint?: string
503
505
  default_dispatch_policy?: DispatchPolicy
504
506
  revision: number
@@ -512,6 +514,7 @@ export interface RegisterEntityTypeRequest {
512
514
  creation_schema?: Record<string, unknown>
513
515
  inbox_schemas?: Record<string, Record<string, unknown>>
514
516
  state_schemas?: Record<string, Record<string, unknown>>
517
+ slash_commands?: Array<SlashCommandDefinition>
515
518
  serve_endpoint?: string
516
519
  default_dispatch_policy?: DispatchPolicy
517
520
  permission_grants?: Array<EntityTypePermissionGrantInput>
@@ -530,6 +533,7 @@ export interface TypedSpawnRequest {
530
533
  */
531
534
  sandbox?: SandboxChoice
532
535
  initialMessage?: unknown
536
+ initialMessageType?: string
533
537
  created_by?: string
534
538
  wake?: {
535
539
  subscriberUrl: string