@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.
@@ -4,7 +4,7 @@ import { DurableStreamTestServer } from "@durable-streams/server";
4
4
  import { createServer } from "node:http";
5
5
  import { createServerAdapter } from "@whatwg-node/server";
6
6
  import { Agent } from "undici";
7
- import { appendPathToUrl, assertTags, buildEventSourceManifestEntry, buildTagsIndex, createEntityRegistry, createRuntimeHandler, entityStateSchema, eventSourceSubscriptionManifestKey, getCronStreamPath, getCronStreamPathFromSpec, getEntitiesStreamPath, getNextCronFireAt, getSharedStateStreamPath, getWebhookStreamPath, hashString, manifestChildKey, manifestSharedStateKey, manifestSourceKey, normalizeTags, parseCronStreamPath, resolveCronScheduleSpec, resolveEventSourceSubscription, sourceRefForTags, verifyWebhookSignature } from "@electric-ax/agents-runtime";
7
+ import { COMPOSER_INPUT_MESSAGE_TYPE, appendPathToUrl, assertTags, buildEventSourceManifestEntry, buildTagsIndex, createEntityRegistry, createRuntimeHandler, 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";
8
8
  import fs, { existsSync } from "node:fs";
9
9
  import path, { dirname, resolve } from "node:path";
10
10
  import { drizzle } from "drizzle-orm/postgres-js";
@@ -64,6 +64,7 @@ const entityTypes = pgTable(`entity_types`, {
64
64
  creationSchema: jsonb(`creation_schema`),
65
65
  inboxSchemas: jsonb(`inbox_schemas`),
66
66
  stateSchemas: jsonb(`state_schemas`),
67
+ slashCommands: jsonb(`slash_commands`),
67
68
  serveEndpoint: text(`serve_endpoint`),
68
69
  defaultDispatchPolicy: jsonb(`default_dispatch_policy`),
69
70
  revision: integer(`revision`).notNull().default(1),
@@ -1152,7 +1153,7 @@ function buildElectricProxyTarget(options) {
1152
1153
  permissionBypass: options.permissionBypass
1153
1154
  }));
1154
1155
  } else if (table === `entity_types`) {
1155
- target.searchParams.set(`columns`, `"tenant_id","name","description","creation_schema","inbox_schemas","state_schemas","serve_endpoint","default_dispatch_policy","revision","created_at","updated_at"`);
1156
+ 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"`);
1156
1157
  applyShapeWhere(target, buildSpawnableEntityTypesWhere({
1157
1158
  tenantId: options.tenantId,
1158
1159
  principalUrl: options.principalUrl ?? ``,
@@ -2725,6 +2726,7 @@ var PostgresRegistry = class {
2725
2726
  creationSchema: et.creation_schema ?? null,
2726
2727
  inboxSchemas: et.inbox_schemas ?? null,
2727
2728
  stateSchemas: et.state_schemas ?? null,
2729
+ slashCommands: et.slash_commands ?? null,
2728
2730
  serveEndpoint: et.serve_endpoint ?? null,
2729
2731
  defaultDispatchPolicy: et.default_dispatch_policy ?? null,
2730
2732
  revision: et.revision,
@@ -2737,6 +2739,7 @@ var PostgresRegistry = class {
2737
2739
  creationSchema: et.creation_schema ?? null,
2738
2740
  inboxSchemas: et.inbox_schemas ?? null,
2739
2741
  stateSchemas: et.state_schemas ?? null,
2742
+ slashCommands: et.slash_commands ?? null,
2740
2743
  serveEndpoint: et.serve_endpoint ?? null,
2741
2744
  defaultDispatchPolicy: et.default_dispatch_policy ?? null,
2742
2745
  revision: et.revision,
@@ -2754,6 +2757,7 @@ var PostgresRegistry = class {
2754
2757
  creationSchema: et.creation_schema ?? null,
2755
2758
  inboxSchemas: et.inbox_schemas ?? null,
2756
2759
  stateSchemas: et.state_schemas ?? null,
2760
+ slashCommands: et.slash_commands ?? null,
2757
2761
  serveEndpoint: et.serve_endpoint ?? null,
2758
2762
  defaultDispatchPolicy: et.default_dispatch_policy ?? null,
2759
2763
  revision: et.revision,
@@ -2780,6 +2784,7 @@ var PostgresRegistry = class {
2780
2784
  creationSchema: et.creation_schema ?? null,
2781
2785
  inboxSchemas: et.inbox_schemas ?? null,
2782
2786
  stateSchemas: et.state_schemas ?? null,
2787
+ slashCommands: et.slash_commands ?? null,
2783
2788
  serveEndpoint: et.serve_endpoint ?? null,
2784
2789
  defaultDispatchPolicy: et.default_dispatch_policy ?? null,
2785
2790
  revision: et.revision,
@@ -3370,6 +3375,7 @@ var PostgresRegistry = class {
3370
3375
  creation_schema: row.creationSchema,
3371
3376
  inbox_schemas: row.inboxSchemas,
3372
3377
  state_schemas: row.stateSchemas,
3378
+ slash_commands: row.slashCommands ?? void 0,
3373
3379
  serve_endpoint: row.serveEndpoint ?? void 0,
3374
3380
  default_dispatch_policy: row.defaultDispatchPolicy ?? void 0,
3375
3381
  revision: row.revision,
@@ -3696,6 +3702,7 @@ var EntityManager = class {
3696
3702
  this.validateSchema(req.creation_schema);
3697
3703
  this.validateSchemaMap(req.inbox_schemas);
3698
3704
  this.validateSchemaMap(req.state_schemas);
3705
+ this.validateSlashCommands(req.slash_commands);
3699
3706
  const defaultDispatchPolicy = req.default_dispatch_policy ? this.validateDispatchPolicy(req.default_dispatch_policy, { label: `default_dispatch_policy` }) : void 0;
3700
3707
  const existing = await this.registry.getEntityType(req.name);
3701
3708
  const now = new Date().toISOString();
@@ -3705,6 +3712,7 @@ var EntityManager = class {
3705
3712
  creation_schema: req.creation_schema,
3706
3713
  inbox_schemas: req.inbox_schemas,
3707
3714
  state_schemas: req.state_schemas,
3715
+ slash_commands: req.slash_commands,
3708
3716
  serve_endpoint: req.serve_endpoint,
3709
3717
  default_dispatch_policy: defaultDispatchPolicy,
3710
3718
  revision: existing ? existing.revision + 1 : 1,
@@ -3866,6 +3874,18 @@ var EntityManager = class {
3866
3874
  }
3867
3875
  });
3868
3876
  const initialEvents = [createdEvent];
3877
+ const slashCommandTimestamp = new Date().toISOString();
3878
+ for (const command of entityType.slash_commands ?? []) {
3879
+ const slashCommandEvent = entityStateSchema.slashCommands.insert({
3880
+ key: command.name,
3881
+ value: {
3882
+ ...command,
3883
+ source: `static`,
3884
+ updated_at: slashCommandTimestamp
3885
+ }
3886
+ });
3887
+ initialEvents.push(slashCommandEvent);
3888
+ }
3869
3889
  if (req.initialMessage !== void 0) {
3870
3890
  const msgNow = new Date().toISOString();
3871
3891
  const inboxEvent = entityStateSchema.inbox.insert({
@@ -3873,6 +3893,7 @@ var EntityManager = class {
3873
3893
  value: {
3874
3894
  from: req.created_by ?? req.parent ?? `spawn`,
3875
3895
  payload: req.initialMessage,
3896
+ message_type: req.initialMessageType,
3876
3897
  timestamp: msgNow
3877
3898
  }
3878
3899
  });
@@ -3952,9 +3973,10 @@ var EntityManager = class {
3952
3973
  const workLocks = new Set();
3953
3974
  const writeEntityLocks = new Set();
3954
3975
  const writeStreamLocks = new Set();
3976
+ const usePointerPath = opts.forkPointer !== void 0 || opts.anchor !== void 0;
3955
3977
  try {
3956
3978
  let sourceTree;
3957
- if (opts.forkPointer) {
3979
+ if (usePointerPath) {
3958
3980
  const rootEntity = await this.registry.getEntity(rootUrl);
3959
3981
  if (!rootEntity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
3960
3982
  if (isTerminalEntityStatus(rootEntity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Cannot fork terminal entity "${rootEntity.url}"`, 409);
@@ -3963,10 +3985,13 @@ var EntityManager = class {
3963
3985
  const sourceRoot = sourceTree[0];
3964
3986
  if (sourceRoot.parent) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Only top-level entities can be forked`, 400);
3965
3987
  let preFilteredRoot;
3966
- if (opts.forkPointer) {
3988
+ let effectiveForkPointer = opts.forkPointer;
3989
+ if (usePointerPath) {
3967
3990
  const sourceEvents = await this.streamClient.readJson(sourceRoot.streams.main);
3968
3991
  const flat = sourceEvents.flatMap((item) => Array.isArray(item) ? item : [item]);
3969
- const target = this.resolveForkPointerTarget(flat, opts.forkPointer, sourceRoot.streams.main);
3992
+ if (!effectiveForkPointer && opts.anchor === `latest_completed_run`) effectiveForkPointer = this.resolveLatestCompletedRunPointer(flat, sourceRoot.streams.main);
3993
+ if (!effectiveForkPointer) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Internal: pointer-path fork with no resolved pointer`, 500);
3994
+ const target = this.resolveForkPointerTarget(flat, effectiveForkPointer, sourceRoot.streams.main);
3970
3995
  const filteredEvents = flat.slice(0, target);
3971
3996
  const rootManifests = this.reduceStateRows(filteredEvents, `manifest`);
3972
3997
  const sharedStateIds = new Set();
@@ -3979,7 +4004,7 @@ var EntityManager = class {
3979
4004
  };
3980
4005
  }
3981
4006
  const effectiveSubtree = preFilteredRoot ? this.computeEffectiveSubtree(sourceTree, sourceRoot.url, preFilteredRoot.manifests) : sourceTree;
3982
- if (opts.forkPointer) {
4007
+ if (usePointerPath) {
3983
4008
  const descendants = effectiveSubtree.filter((entity) => entity.url !== sourceRoot.url);
3984
4009
  if (descendants.length > 0) await this.waitForGivenEntitiesIdle(descendants, opts, workLocks);
3985
4010
  }
@@ -4004,6 +4029,14 @@ var EntityManager = class {
4004
4029
  const sharedStateIdMap = await this.buildForkSharedStateIdMap(snapshot.sharedStateIds, suffix);
4005
4030
  const stringMap = this.buildForkStringMap(entityUrlMap, sharedStateIdMap);
4006
4031
  const entityPlans = this.buildForkEntityPlans(effectiveSubtree, entityUrlMap, stringMap, opts.createdBy);
4032
+ const rootPlanForOverrides = entityPlans.find((plan) => plan.source.url === rootUrl);
4033
+ if (rootPlanForOverrides) {
4034
+ if (opts.parent !== void 0) rootPlanForOverrides.fork.parent = opts.parent;
4035
+ if (opts.tags !== void 0) rootPlanForOverrides.fork.tags = {
4036
+ ...rootPlanForOverrides.fork.tags ?? {},
4037
+ ...opts.tags
4038
+ };
4039
+ }
4007
4040
  this.addForkLocks(this.forkWriteLockedEntities, effectiveSubtree.map((entity) => entity.url), writeEntityLocks);
4008
4041
  this.addForkLocks(this.forkWriteLockedStreams, [...snapshot.sharedStateIds].map((id) => getSharedStateStreamPath(id)), writeStreamLocks);
4009
4042
  const createdStreams = [];
@@ -4012,7 +4045,7 @@ var EntityManager = class {
4012
4045
  try {
4013
4046
  for (const plan of entityPlans) {
4014
4047
  const isRoot = plan.source.url === rootUrl;
4015
- await this.streamClient.fork(plan.fork.streams.main, plan.source.streams.main, isRoot && opts.forkPointer ? { forkPointer: opts.forkPointer } : void 0);
4048
+ await this.streamClient.fork(plan.fork.streams.main, plan.source.streams.main, isRoot && effectiveForkPointer ? { forkPointer: effectiveForkPointer } : void 0);
4016
4049
  createdStreams.push(plan.fork.streams.main);
4017
4050
  }
4018
4051
  for (const [sourceId, forkId] of sharedStateIdMap) {
@@ -4039,6 +4072,20 @@ var EntityManager = class {
4039
4072
  await this.registry.createEntity(plan.fork);
4040
4073
  createdEntities.push(plan.fork.url);
4041
4074
  }
4075
+ if (opts.wake !== void 0) {
4076
+ const rootPlan = entityPlans.find((plan) => plan.source.url === rootUrl);
4077
+ if (rootPlan) await this.wakeRegistry.register({
4078
+ tenantId: this.tenantId,
4079
+ subscriberUrl: opts.wake.subscriberUrl,
4080
+ sourceUrl: rootPlan.fork.url,
4081
+ condition: opts.wake.condition,
4082
+ debounceMs: opts.wake.debounceMs,
4083
+ timeoutMs: opts.wake.timeoutMs,
4084
+ oneShot: false,
4085
+ includeResponse: opts.wake.includeResponse,
4086
+ manifestKey: opts.wake.manifestKey
4087
+ });
4088
+ }
4042
4089
  for (const plan of entityPlans) {
4043
4090
  const manifests = activeManifestsByEntity.get(plan.fork.url) ?? new Map();
4044
4091
  await this.materializeForkManifestSideEffects(plan.fork.url, manifests);
@@ -4199,6 +4246,45 @@ var EntityManager = class {
4199
4246
  return positionAtAnchor + pointer.subOffset;
4200
4247
  }
4201
4248
  /**
4249
+ * Find an `EventPointer` that addresses the most recent `runs` row on
4250
+ * the source's `main` stream with `status === 'completed'`. Mirrors the
4251
+ * eligibility rule the per-row "Fork from here" UI applies — only
4252
+ * completed runs are valid fork anchors; `started`/`failed` rows are
4253
+ * skipped.
4254
+ *
4255
+ * The pointer is computed in the same coordinate system the runtime
4256
+ * mints pointers in (see entity-stream-db's onBeforeBatch): for a row
4257
+ * R in log entry E, the pointer is `{ offset: P, subOffset: K }` where
4258
+ * `P` is the END offset of the log entry preceding E (or `null` for
4259
+ * stream start) and `K` is R's 1-indexed position within E.
4260
+ */
4261
+ resolveLatestCompletedRunPointer(events, streamPath) {
4262
+ let priorEntryOffset = null;
4263
+ let currentEntryOffset = null;
4264
+ let positionInEntry = 0;
4265
+ let latest = null;
4266
+ for (const event of events) {
4267
+ const headers = isRecord(event.headers) ? event.headers : void 0;
4268
+ const eventOffset = typeof headers?.offset === `string` ? headers.offset : null;
4269
+ if (eventOffset !== currentEntryOffset) {
4270
+ priorEntryOffset = currentEntryOffset;
4271
+ currentEntryOffset = eventOffset;
4272
+ positionInEntry = 0;
4273
+ }
4274
+ positionInEntry++;
4275
+ if (event.type !== `run`) continue;
4276
+ if (headers?.operation === `delete`) continue;
4277
+ if (!isRecord(event.value)) continue;
4278
+ if (event.value.status !== `completed`) continue;
4279
+ latest = {
4280
+ offset: priorEntryOffset,
4281
+ subOffset: positionInEntry
4282
+ };
4283
+ }
4284
+ if (!latest) throw new ElectricAgentsError(ErrCodeInvalidRequest, `Source ${streamPath} has no completed run to fork from`, 400);
4285
+ return latest;
4286
+ }
4287
+ /**
4202
4288
  * Compute the subset of `sourceTree` that survives the manifest filter
4203
4289
  * applied at the root. After filtering the root's manifest at the fork
4204
4290
  * pointer, only children whose manifest entries landed at or before the
@@ -5318,7 +5404,9 @@ var EntityManager = class {
5318
5404
  creation_schema: existing.creation_schema,
5319
5405
  inbox_schemas: mergedInbox,
5320
5406
  state_schemas: mergedState,
5407
+ slash_commands: existing.slash_commands,
5321
5408
  serve_endpoint: existing.serve_endpoint,
5409
+ default_dispatch_policy: existing.default_dispatch_policy,
5322
5410
  revision: nextRevision,
5323
5411
  created_at: existing.created_at,
5324
5412
  updated_at: now
@@ -5372,11 +5460,19 @@ var EntityManager = class {
5372
5460
  throw new ElectricAgentsError(ErrCodeInvalidRequest, error instanceof Error ? error.message : `Invalid tags`, 400);
5373
5461
  }
5374
5462
  }
5463
+ validateSlashCommands(input) {
5464
+ const validationError = validateSlashCommandDefinitions(input);
5465
+ if (!validationError) return;
5466
+ throw new ElectricAgentsError(ErrCodeSchemaValidationFailed, validationError.message, 422, validationError.details);
5467
+ }
5375
5468
  async validateSendRequest(entityUrl, req) {
5376
5469
  const entity = await this.registry.getEntity(entityUrl);
5377
5470
  if (!entity) throw new ElectricAgentsError(ErrCodeNotFound, `Entity not found`, 404);
5378
5471
  if (rejectsNormalWrites(entity.status)) throw new ElectricAgentsError(ErrCodeNotRunning, `Entity is not accepting writes`, 409);
5379
- if (req.type && entity.type) {
5472
+ if (req.type === COMPOSER_INPUT_MESSAGE_TYPE) {
5473
+ const valErr = validateComposerInputPayload(req.payload);
5474
+ if (valErr) throw new ElectricAgentsError(ErrCodeSchemaValidationFailed, valErr.message, 422, valErr.details);
5475
+ } else if (req.type && entity.type) {
5380
5476
  const { inboxSchemas } = await this.getEffectiveSchemas(entity);
5381
5477
  if (inboxSchemas) {
5382
5478
  const schema = inboxSchemas[req.type];
@@ -5657,6 +5753,7 @@ const spawnBodySchema = Type.Object({
5657
5753
  sandbox: Type.Optional(sandboxChoiceSchema),
5658
5754
  initialMessage: Type.Optional(Type.Unknown()),
5659
5755
  grants: Type.Optional(Type.Array(entityPermissionGrantInputSchema)),
5756
+ initialMessageType: Type.Optional(Type.String()),
5660
5757
  wake: Type.Optional(Type.Object({
5661
5758
  subscriberUrl: Type.String(),
5662
5759
  condition: wakeConditionSchema,
@@ -5694,6 +5791,14 @@ function agentUrlPath(value) {
5694
5791
  return value;
5695
5792
  }
5696
5793
  }
5794
+ async function hasValidAgentWriteToken(request, ctx, fromAgent) {
5795
+ const agentUrl = agentUrlPath(fromAgent);
5796
+ const token = writeTokenFromRequest(request);
5797
+ if (!token) return false;
5798
+ const agentEntity = await ctx.entityManager.registry.getEntity(agentUrl);
5799
+ if (!agentEntity) return false;
5800
+ return ctx.entityManager.isValidWriteToken(agentEntity, token);
5801
+ }
5697
5802
  const inboxMessageBodySchema = Type.Object({
5698
5803
  payload: Type.Optional(Type.Unknown()),
5699
5804
  position: Type.Optional(Type.String()),
@@ -5715,7 +5820,19 @@ const forkBodySchema = Type.Object({
5715
5820
  fork_pointer: Type.Optional(Type.Object({
5716
5821
  offset: Type.Union([Type.String(), Type.Null()]),
5717
5822
  sub_offset: Type.Number()
5718
- }))
5823
+ })),
5824
+ anchor: Type.Optional(Type.Literal(`latest_completed_run`)),
5825
+ parent: Type.Optional(Type.String()),
5826
+ wake: Type.Optional(Type.Object({
5827
+ subscriberUrl: Type.String(),
5828
+ condition: wakeConditionSchema,
5829
+ debounceMs: Type.Optional(Type.Number()),
5830
+ timeoutMs: Type.Optional(Type.Number()),
5831
+ includeResponse: Type.Optional(Type.Boolean()),
5832
+ manifestKey: Type.Optional(Type.String())
5833
+ })),
5834
+ initialMessage: Type.Optional(Type.Unknown()),
5835
+ tags: Type.Optional(stringRecordSchema$1)
5719
5836
  });
5720
5837
  const setTagBodySchema = Type.Object({ value: Type.String() });
5721
5838
  const entitySignalSchema = Type.Union([
@@ -6102,8 +6219,18 @@ async function forkEntity(request, ctx) {
6102
6219
  const principalMutationError = rejectPrincipalEntityMutation(request, `forked`);
6103
6220
  if (principalMutationError) return principalMutationError;
6104
6221
  const parsed = routeBody(request);
6222
+ if (parsed.fork_pointer && parsed.anchor) return apiError(400, ErrCodeInvalidRequest, `fork_pointer and anchor are mutually exclusive`);
6105
6223
  const { entityUrl, entity } = requireExistingEntityRoute(request);
6106
6224
  await assertDispatchPolicyAllowed(ctx, entity.dispatch_policy);
6225
+ if (parsed.parent !== void 0) {
6226
+ const parent = await ctx.entityManager.registry.getEntity(parsed.parent);
6227
+ if (!parent) return apiError(404, ErrCodeNotFound, `Parent entity not found`);
6228
+ if (!await canAccessEntity(ctx, parent, `spawn`, request)) return apiError(401, ErrCodeUnauthorized, `Principal is not allowed to spawn children from ${parent.url}`);
6229
+ }
6230
+ if (parsed.wake !== void 0) {
6231
+ if (parsed.parent === void 0) return apiError(400, ErrCodeInvalidRequest, `wake requires parent (the fork's wake fires to its parent)`);
6232
+ if (parsed.wake.subscriberUrl !== parsed.parent) return apiError(401, ErrCodeUnauthorized, `wake.subscriberUrl must match parent`);
6233
+ }
6107
6234
  const result = await ctx.entityManager.forkSubtree(entityUrl, {
6108
6235
  rootInstanceId: parsed.instance_id,
6109
6236
  waitTimeoutMs: parsed.waitTimeoutMs,
@@ -6111,9 +6238,17 @@ async function forkEntity(request, ctx) {
6111
6238
  ...parsed.fork_pointer && { forkPointer: {
6112
6239
  offset: parsed.fork_pointer.offset,
6113
6240
  subOffset: parsed.fork_pointer.sub_offset
6114
- } }
6241
+ } },
6242
+ ...parsed.anchor && { anchor: parsed.anchor },
6243
+ ...parsed.parent !== void 0 && { parent: parsed.parent },
6244
+ ...parsed.wake !== void 0 && { wake: parsed.wake },
6245
+ ...parsed.tags !== void 0 && { tags: parsed.tags }
6115
6246
  });
6116
6247
  for (const forkedEntity of result.entities) await linkEntityDispatchSubscription(ctx, forkedEntity);
6248
+ if (parsed.initialMessage !== void 0) await ctx.entityManager.send(result.root.url, {
6249
+ from: parsed.parent ?? ctx.principal.url,
6250
+ payload: parsed.initialMessage
6251
+ });
6117
6252
  return json({
6118
6253
  root: toPublicEntity(result.root),
6119
6254
  entities: result.entities.map((entity$1) => toPublicEntity(entity$1))
@@ -6126,7 +6261,10 @@ async function sendEntity(request, ctx) {
6126
6261
  if (parsed.from_principal !== void 0 && parsed.from_principal !== principal.url) return apiError(400, ErrCodeInvalidRequest, `Request from_principal must match Electric-Principal`);
6127
6262
  if (parsed.from_agent !== void 0) {
6128
6263
  const principalAgentUrl = agentUrlForPrincipal(principal);
6129
- if (agentUrlPath(parsed.from_agent) !== principalAgentUrl) return apiError(400, ErrCodeInvalidRequest, `Request from_agent must match authenticated agent principal`);
6264
+ const fromAgentUrl = agentUrlPath(parsed.from_agent);
6265
+ const matchesPrincipalAgent = fromAgentUrl === principalAgentUrl;
6266
+ const hasAgentWriteToken = await hasValidAgentWriteToken(request, ctx, parsed.from_agent);
6267
+ if (!matchesPrincipalAgent && !hasAgentWriteToken) return apiError(400, ErrCodeInvalidRequest, `Request from_agent must match authenticated agent principal`);
6130
6268
  }
6131
6269
  await ctx.entityManager.ensurePrincipal(principal);
6132
6270
  const { entityUrl, entity } = requireExistingEntityRoute(request);
@@ -6212,6 +6350,7 @@ async function spawnEntity(request, ctx) {
6212
6350
  dispatch_policy: dispatchPolicy,
6213
6351
  sandbox: parsed.sandbox,
6214
6352
  initialMessage: void 0,
6353
+ initialMessageType: void 0,
6215
6354
  wake: parsed.wake,
6216
6355
  created_by: principal.url
6217
6356
  });
@@ -6230,7 +6369,8 @@ async function spawnEntity(request, ctx) {
6230
6369
  if (linkBeforeInitialMessage) await linkEntityDispatchSubscription(ctx, entity);
6231
6370
  if (parsed.initialMessage !== void 0) await ctx.entityManager.send(entity.url, {
6232
6371
  from: principal.url,
6233
- payload: parsed.initialMessage
6372
+ payload: parsed.initialMessage,
6373
+ type: parsed.initialMessageType
6234
6374
  });
6235
6375
  if (!linkBeforeInitialMessage) await linkEntityDispatchSubscription(ctx, entity);
6236
6376
  return json({
@@ -6277,6 +6417,21 @@ async function signalEntity(request, ctx) {
6277
6417
  //#region src/routing/entity-types-router.ts
6278
6418
  const jsonObjectSchema = Type.Record(Type.String(), Type.Unknown());
6279
6419
  const schemaMapSchema = Type.Record(Type.String(), jsonObjectSchema);
6420
+ const slashCommandArgumentSchema = Type.Object({
6421
+ name: Type.String(),
6422
+ type: Type.Union([
6423
+ Type.Literal(`string`),
6424
+ Type.Literal(`number`),
6425
+ Type.Literal(`boolean`)
6426
+ ]),
6427
+ required: Type.Optional(Type.Boolean()),
6428
+ description: Type.Optional(Type.String())
6429
+ }, { additionalProperties: false });
6430
+ const slashCommandSchema = Type.Object({
6431
+ name: Type.String(),
6432
+ description: Type.Optional(Type.String()),
6433
+ arguments: Type.Optional(Type.Array(slashCommandArgumentSchema))
6434
+ }, { additionalProperties: false });
6280
6435
  const typePermissionGrantInputSchema = Type.Object({
6281
6436
  subject_kind: Type.Union([Type.Literal(`principal`), Type.Literal(`principal_kind`)]),
6282
6437
  subject_value: Type.String(),
@@ -6289,6 +6444,7 @@ const registerEntityTypeBodySchema = Type.Object({
6289
6444
  creation_schema: Type.Optional(jsonObjectSchema),
6290
6445
  inbox_schemas: Type.Optional(schemaMapSchema),
6291
6446
  state_schemas: Type.Optional(schemaMapSchema),
6447
+ slash_commands: Type.Optional(Type.Array(slashCommandSchema)),
6292
6448
  serve_endpoint: Type.Optional(Type.String()),
6293
6449
  default_dispatch_policy: Type.Optional(dispatchPolicySchema),
6294
6450
  permission_grants: Type.Optional(Type.Array(typePermissionGrantInputSchema))
@@ -6430,6 +6586,7 @@ function normalizeEntityTypeRequest(parsed) {
6430
6586
  creation_schema: parsed.creation_schema,
6431
6587
  inbox_schemas: parsed.inbox_schemas,
6432
6588
  state_schemas: parsed.state_schemas,
6589
+ slash_commands: parsed.slash_commands,
6433
6590
  serve_endpoint: serveEndpoint,
6434
6591
  default_dispatch_policy: parsed.default_dispatch_policy ?? (serveEndpoint ? { targets: [{
6435
6592
  type: `webhook`,